팀 이름
직원 이름
팀의 매니저인지 매니저가 아닌지 여부
회사에 들어온 일자
생일
[
{
"name": "팀 이름",
"manager": "팀 매니저 이름" // 없을 경우 null
"memberCount": 팀 인원 수 [숫자]
}, ...
]
[
{
"name" : "직원 이름",
"teamName" : "소속 팀 이름",
"role": "MANAGER" or "MEMBER",
"birthday": "1989-01-01",
"workStartDate": "2024-01-01",
}, ...
]
Table
💡 고민 1.
테이블을 어떻게 짤까?
CREATE TABLE member
(
id bigint auto_increment,
name varchar(20),
team_id bigint,
team_name varchar(20),
role tinyint,
birthday datetime,
work_start_date datetime,
primary key (id)
);
CREATE TABLE team
(
id bigint auto_increment,
name varchar(20),
manager varchar(20),
primary key (id)
);
⚠️ 맨 처음에는
CREATE TABLE team ( id bigint auto_increment, name varchar(20), primary key (id) );
이런식으로
manager
컬럼을 따로 저장하지 않았는데, ③팀 조회
기능을 만들다 보니
팀 조회
시 매번 매니저를 찾기위해member
를 조회하는게 좀 마음에 안 들기도 했고,
②직원 등록
기능에서도team.teamManger
의 값이 null 인 경우,
굳이findByTeamIdAndRoleIsTrue
를 통해 이미 존재하는 매니저가 있는지 검사하는 과정을
거치지 않아도 된다는 점에서 이점이 있다고 생각해서manager
컬럼을 추가하였습니다.
Controller
💡
Team
,Member
생성시에 HttpStatus로 201을 반환하는거 말고는 딱히 특이사항 없습니다.
DTO(Request)
@Getter
public class CreateTeamRequest {
...
public Team toEntity(){
return Team.builder()
.name(name)
.build();
}
}
@Getter
public class SaveMemberRequest {
private String name;
private String teamname;
private Boolean isManager;
private LocalDate birthday;
public Member toEntity(Team team) {
return Member.builder()
.name(this.name)
.teamName(team.getName())
.role(isManager)
.birthday(this.birthday)
.team(team)
.build();
}
}
💡
toEntity
과정에서member
:team
을 매핑처리 해주었습니다.
Domain
💡
@builder
를 사용한 이유 : 개발 6개월차라DTO
나 데이터베이스Column
,
Entity
의 필드 등이 수시로 변하는데,builder
를 사용하면 필드의 순서 변경에 영향을 받지 않고,
builder
를 통한 변환과정에서
변경이 필요한 부분은 붉은줄로 알려줘서 직관적인 수정이 가능하다는 점에서 이점이 있다고 생각하여 사용하였습니다.
@Entity
@Getter
public class Team {
protected Team() {}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String name;
private String manager;
@OneToMany(mappedBy = "team")
List<Member> memberList = new ArrayList<>();
@Builder
public Team(String name) {
this.name = name;
}
public void updateManager(String manager) {
this.manager = manager;
}
public GetAllTeamsResponse toResponse() {
return GetAllTeamsResponse.builder()
.name(name)
.manager(manager)
.memberCount(memberList.size())
.build();
}
}
@Entity
@Getter
@EntityListeners(AuditingEntityListener.class)
public class Member {
protected Member() {}
@Id
@Column
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String name;
private String teamName;
private boolean role;
private LocalDate birthday;
@CreatedDate
private LocalDateTime workStartDate;
@ManyToOne
private Team team;
public GetAllMembersResponse toResponse() {
String isManager = this.role ? "MANAGER" : "MEMBER"; // true -> manager / false -> member
return GetAllMembersResponse.builder()
.name(name)
.teamName(teamName)
.role(isManager)
.birthday(birthday)
.workStartDate(workStartDate)
.build();
}
@Builder
public Member(String name, String teamName, boolean role, LocalDate birthday, Team team) {
this.name = name;
this.teamName = teamName;
this.role = role;
this.birthday = birthday;
this.team = team;
}
public void changeRole() {
this.role = !this.role;
} //true -> false / false -> true
}
💡
workStartDate
-> 생성날짜로 하기 위해@CreatedDate
을 사용하였고,
Team
:Member
를1 : N
양방향 연관관계로 맺어주었습니다.
💡 고민 2.
Entity
와Domain
을 분리시켜야하나 고민중입니다.
하지만 JPA를 공부하기 위한 목적으로 미니 프로젝트를 수행중인데,
분리로 인해 JPA의 특장점 중 하나인 변경 감지 기능을 사용할 수 없다는게 마음에 걸립니다.
sevice
@Service
public class MemberService {
private final MemberRepository memberRepository;
private final TeamService teamService;
public MemberService(MemberRepository memberRepository, TeamService teamService) {
this.memberRepository = memberRepository;
this.teamService = teamService;
}
@Transactional
public void saveMember(SaveMemberRequest request) {
Team team = teamService.findTeamByName(request); //teamService를 통해 조회합니다.
memberRepository.save(request.toEntity(team));
}
public List<GetAllMembersResponse> getAllMembers() {
return memberRepository.findAll().stream().map(Member::toResponse).toList();
}
}
@Service
public class TeamService {
private final TeamRepository teamRepository;
private final MemberRepository memberRepository;
public TeamService(TeamRepository teamRepository, MemberRepository memberRepository) {
this.teamRepository = teamRepository;
this.memberRepository = memberRepository;
}
@Transactional
public void createTeam(CreateTeamRequest request) {
if (teamRepository.existsByName(request.getName())) throw new IllegalArgumentException("존재하는 팀입니다.");
teamRepository.save(request.toEntity());
}
public Team findTeamByName(SaveMemberRequest request) {
Team team = teamRepository.findByName(request.getTeamname()).orElseThrow(IllegalArgumentException::new);
if (request.getIsManager())
updateManager(team, request.getName());
//request가 isManager==true면 팀의 새로운 매니저로 업데이트합니다.
return team;
}
public void updateManager(Team team, String managerName) {
if (team.getManager() == null) team.updateManager(managerName);
else {
Member member = memberRepository.findByTeamIdAndRoleIsTrue(team.getId())
.orElseThrow(IllegalArgumentException::new);
member.changeRole();
team.updateManager(managerName);
}
public List<GetAllTeamsResponse> getAllTeams() {
return teamRepository.findAll().stream().map(Team::toResponse).toList();
}
}
💡
.collect(Collectors.toList())
대신.toList()
사용한 이유
반환 타입인response리스트
에 대한 추가적인 수정의 필요성이 적다 생각하여toList
를 사용하였습니다.
⚠️ 문제 발생
원래는 TeamService의updateManager
에서
memberService
.findByTeamIdAndRoleIsTrue(team.getId()) 이런식으로 MemberService를 통해 가져오려 했으나,
순환 참조 문제가 발생하였습니다.
(MemberController -> MemberService / MemberService -> TeamService / TeamService -> MemberController)@Service public class MemberService{ ~~ ... public Member findByTeamIdAndRoleIsTrue(long id){ return memberRepository.findByTeamIdAndRoleIsTrue(id).orElseThrow(IllegalArgumentException::new); } ~~ ... }
💡 해결
TeamService
가MemberService
를 의존하던걸 끊고, 직접memberRepository
를 주입받아 사용하도록 하였습니다.
DTO(Response)
@Getter
public class GetAllMembersResponse {
public final String name;
public final String teamName;
public final String role;
public final LocalDate birthday;
public final LocalDate workStartDate;
@Builder
public GetAllMembersResponse(String name, String teamName, String role, LocalDate birthday, LocalDateTime workStartDate) {
this.name = name;
this.teamName = teamName;
this.role = role;
this.birthday = birthday;
this.workStartDate = workStartDate.toLocalDate();
}
}
@Getter
public class GetAllTeamsResponse {
private final String name;
private final String manager;
private final int memberCount;
@Builder
public GetAllTeamsResponse(String name, String manager, int memberCount) {
this.name = name;
this.manager = manager;
this.memberCount = memberCount;
}
}
Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByTeamIdAndRoleIsTrue(long id);
}
public interface TeamRepository extends JpaRepository<Team, Long> {
boolean existsByName(String name);
Optional<Team> findByName(String name);
}
-> 매니저가 없으면
null
/memberCount
기능
📌 한 걸음 더!
매니저
변경하기
-> 기존 매니저 일반 멤버로 변경 + 새로운 매니저 등록