강의 출처
기술 스택
- 자바 17 버전
- Spring Boot 3.2.3 버전
- JPA
- MySQL
프로젝트 1단계
프로젝트 생성
DB 설계
- Team 테이블
- name - String - 팀 이름 - 필수
- manager - String - 팀 매니저 이름. null값 허용
- memberCount - int - 팀 인원 수
- Employee 테이블
- name - String - 직원 이름
- teamName - String - 소속 팀 이름
- role - ENUM - 직급(매니저, 멤버)
- birthday - LocalDate - 생일
- workStartDate - LocalDate - 입사일
API 스펙 정하기
팀 등록하기
- 팀을 등록한다.
- HTTP Method > POST
- HTTP Path > /team
- HTTP Param > 쿼리 파라미터 String name
- 반환 타입 > 200 OK
직원 등록하기
- 직원을 등록한다.
- HTTP Mehtod > POST
- HTTP Path > /employee
- HTTP Param > body, String name, String role, String temaName, LocalDate workStartDate, LocalDate birthday
- 반환 타입 > 200 OK
팀 조회하기
- 모든 팀의 정보를 한 번에 조회함
- HTTP Method > GET
- HTTP Path > /teams
- HTTP Param > X
- 반환 타입 > JSON
직원 등록하기
- 모든 직원을 한 번에 조회함
- HTTP Mehtod > GET
- HTTP Path > /empolyees
- HTTP Param > X
- 반환 타입 > JSON
프로젝트 설정
MySQL 설정 + JPA 설정
- build.gradle
// MySQL
runtimeOnly 'com.mysql:mysql-connector-j'
- Database 설정
create database works; // database 생성
use works; // 만든 database 사용
- 인텔리제이 MySQL 연결
- application.yml
spring:
datasource:
url: "jdbc:mysql://localhost/works"
username: "root"
password:
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
show_sql: true
format_sql: true
Team 등록하기 구현
Domain
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name; // 팀 이름
private String manager; // 팀 매니저 이름
private int memberCount; // 팀 인원 수
}
Controller
@PostMapping("/team")
public void addTeam(@RequestParam String name) {
teamService.addTeam(name);
}
Service
@Transactional
public void addTeam(String name) {
teamRepository.save(new Team(name));
}
Repository
public interface TeamRepository extends JpaRepository<Team, Long> {
}
- 포스트맨 테스트
예외 사항 추가
- team 이름이 중복인 경우
TeamService
@Transactional
public void addTeam(String name) {
boolean isDuplicate = teamRepository.existsByName(name);
if(isDuplicate) throw new IllegalArgumentException("팀 이름이 중복되었습니다.");
teamRepository.save(Team.builder().name(name).build());
}
TeamRepository
boolean existsByName(String name);
- 팀 이름이 null이거나 중복인 경우
if(name == null || name.isBlank()) throw new IllegalArgumentException("팀 이름은 빈 값을 저장할 수 없습니다.");
테스트 - 중복일 경우
테스트 - 값이 비어있을 경우
Employee 등록하기 구현
Domain
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name; // 직원 이름
@ManyToOne
@JoinColumn(nullable = false, name = "team_name")
private Team team;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private Role role; // 역할
@Column(nullable = false)
private LocalDate workStartDate; // 입사일
@Column(nullable = false)
private LocalDate birthday; // 생일
}
개발을 해보니까 team의 id보다 name을 키로 갖고 매핑을 하는 게 더 나을 것 같다. id로 매핑하면 직원을 조회할때마다 team의 id로 name을 찾는 추가적인 로직이 들어가야하기 때문..! String으로는 한번도 id를 줘본적이 없어서 맞는 방식인가 싶기도 한데 구글링해보니까 String으로도 주는 것 같아서 일단 Team의 도메인을 수정했다.
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Team {
// @Id
// @GeneratedValue(strategy = GenerationType.IDENTITY)
// private Long id;
@Id
@Column(nullable = false)
private String name; // 팀 이름
private String manager; // 팀 매니저 이름
private int memberCount; // 팀 인원 수
public void setManager(String name) {
this.manager = name;
}
public void addMember() {
this.memberCount++;
}
}
TeamRepository도 수정
public interface TeamRepository extends JpaRepository<Team, String> {
}
TeamService 수정
/**
* 새로운 팀을 등록한다.
* @param name
*/
@Transactional
public void addTeam(String name) {
if(name == null || name.isBlank()) throw new IllegalArgumentException("팀 이름은 빈 값을 저장할 수 없습니다.");
boolean isDuplicate = teamRepository.existsById(name);
if(isDuplicate) throw new IllegalArgumentException("팀 이름이 중복되었습니다.");
teamRepository.save(Team.builder().name(name).build());
}
Controller
@PostMapping
public void addEmployee(@RequestBody EmployeeAddRequest request) {
employeeService.addEmployee(request);
}
Dto
@Getter
@AllArgsConstructor
public class EmployeeAddRequest {
private String name;
private Role role;
private String teamName;
private LocalDate workStartDate;
private LocalDate birthday;
}
Service
/**
* 직원을 등록한다.
* @param request
*/
@Transactional
public void addEmployee(EmployeeAddRequest request) {
// team 객체 가져오기
Team team = teamRepository.findById(request.getTeamName())
.orElseThrow(IllegalArgumentException::new);
employeeRepository.save(Employee.builder()
.name(request.getName())
.team(team)
.role(request.getRole())
.workStartDate(request.getWorkStartDate())
.birthday(request.getBirthday()).build());
// 만약 역할이 매니저면 team에 manager 등록
if(request.getRole() == Role.MANAGER) {
team.setManager(request.getName());
}
//팀 인원 수 + 1
team.addMember();
}
- 포스트맨 테스트
테스트를 하다보니 문제점이 생긴 걸 볼 수 있다. 한 팀에 여러 명의 매니저가 생기는 문제를 해결해줘야할 거 같다.
예외 사항 추가
팀에 팀장은 바뀔 수 있는 존재니까 기존 팀에 MANAGER가 있다고 새로운 팀장을 저장못하면 말이 안되니 지금 저장 형태는 맞다고 생각된다. 다만 새로운 MANAGER가 등록이 됐을 때 기존 MANAGER였던 직원의 역할을 MEMBER로 변경해주자.
Team domain 클래스에 역할을 바꿔주는 changeRole() 메서드를 추가했다. Team이 담긴 employee List가 필요해서 필드를 추가하고 @OneToMany로 매핑해주었다.
@OneToMany(mappedBy = "team")
private List<Employee> employees = new ArrayList<>();
그리고 현재 기준 역할이 변경되는 순간은 EmployeeService에서 Team의 setManager() 밖에 없으니 Team의 setManager()에서 changeRole()를 호출하게 했다.
public void setManager(String name) {
changeRole();
this.manager = name;
}
private void changeRole() {
if(this.manager == null) return;
Employee findEmployee = this.employees.stream()
.filter(employee -> employee.getRole() == Role.MANAGER)
.findFirst()
.orElseThrow(IllegalArgumentException::new);
findEmployee.changeRole(Role.MEMBER);
}
Employee domain에서도 changeRole()을 추가했다.
public void changeRole(Role role) {
this.role = role;
}
- 포스트맨 테스트
employee 기존 데이터 삭제 후 다시 처음부터 넣어줬다.
잘 변경된 것을 확인할 수 있다.
Team 조회하기 구현
Controller
@GetMapping("/teams")
public List<TeamListResponse> getTeamList() {
return teamService.getTeamList();
}
Dto
@Getter
@AllArgsConstructor
public class TeamListResponse {
private String name;
private String manager;
private int memberCount;
public TeamListResponse(Team team) {
this.name = team.getName();
this.manager = team.getManager();
this.memberCount = team.getMemberCount();
}
}
Service
/**
* 모든 팀을 조회한다.
* @return List
*/
@Transactional(readOnly = true)
public List<TeamListResponse> getTeamList() {
return teamRepository.findAll().stream()
.map(TeamListResponse::new)
.collect(Collectors.toList());
}
- 포스트맨 테스트
조회하기 전에 team 데이터를 두어개 더 넣어놓자.
team 조회를 해보면!
Employee 조회하기 구현
Controller
@GetMapping("/employees")
public List<EmployeeListResponse> getEmployeeList() {
return employeeService.getEmployeeList();
}
Dto
@Getter
@AllArgsConstructor
public class EmployeeListResponse {
private String name;
private String teamName;
private Role role;
private LocalDate birthday;
private LocalDate workStartDate;
public EmployeeListResponse(Employee employee) {
this.name = employee.getName();
this.teamName = employee.getTeam().getName();
this.role = employee.getRole();
this.birthday = employee.getBirthday();
this.workStartDate = employee.getWorkStartDate();
}
}
Service
/**
* 모든 직원을 조회한다.
* @return List
*/
public List<EmployeeListResponse> getEmployeeList() {
return employeeRepository.findAll().stream()
.map(EmployeeListResponse::new)
.collect(Collectors.toList());
}
- 포스트맨 테스트
조회하기 전에 데이터를 두어개 정도 추가적으로 넣어주자.
수치스러운 404 에러~!
컨트롤러를 확인해보니 클래스 레벨에 @RequestMapping("/employee")를 냅다 써버려서 요청 url를 받아들이지 못한거였다. 수정하고 다시 조회!
추가적으로 발견한 에러 사항
직원을 등록할 때 JSON의 role이 대문자여야지만 맵핑이 가능하다.
manager, member가 대문자가 아니면 바인딩 관련 에러가 발생한다.
스프링에서 제공하는 StringToEnumConverter가 아래와 같이 동작하기 때문이다.
값에 대한 별다른 컨트롤이 없다! 그래서 대소문자를 구분하지 않게 만드려면 컨버터를 추가하는 방법, 아니면 아래와 같이 application.yml에 설정을 추가해주면 된다.
spring:
jackson:
mapper:
accept-case-insensitive-enums: true
다시 테스트!
다음 포스트
Reference
- https://blog.naver.com/PostView.naver?blogId=simpolor&logNo=221598980908