[미니 프로젝트] _ Step 1.

김영훈·2024년 2월 27일
0

Project

목록 보기
1/6
post-thumbnail

구현 내용

  • ①팀 등록 기능
    • 회사에 있는 팀을 등록할 수 있어야 합니다. 팀이 가져야 할 필수 정보는 다음과 같습니다
    • 팀 이름

  • ②직원 등록 기능
    직원을 등록할 수 있어야합니다. 직원이 가져야할 필수 정보 :
    직원 이름
    팀의 매니저인지 매니저가 아닌지 여부
    회사에 들어온 일자
    * 생일

  • ③팀 조회 기능
    * 모든 팀의 정보를 한 번에 조회할 수 있어야 합니다.
[
	{
		"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 : Member1 : N 양방향 연관관계로 맺어주었습니다.

💡 고민 2.
EntityDomain을 분리시켜야하나 고민중입니다.
하지만 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);
    }
    ~~ ...
}

💡 해결
TeamServiceMemberService를 의존하던걸 끊고, 직접 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 기능

  • ④직원 조회 기능

📌 한 걸음 더!
매니저 변경하기

  • ⑤매니저가 존재하는 상황에서의 매니저 등록!

-> 기존 매니저 일반 멤버로 변경 + 새로운 매니저 등록


0개의 댓글