Member는 ROLE이라는 Enum으로 나누어진 User이므로 UserRository를 사용합니다.

1. MemberService에서 사용될 TeamRepository, RequestTeamRepsitory먼저 만들어줍니다.

public interface RequestTeamRepository extends JpaRepository<RequestTeam, Long> {

}
public interface TeamRepository extends JpaRepository<Team, Long> {
    
}

 

2. MemberService, MemberServiceImpl에서 필요로 하는 로직 먼저 살펴봅시다. 

  • Member는 팀에 가입 요청을 할 수 있다. (단 한 팀에게 한 번 요청할 수 있다.) 
  • Member는 가입요청 한 것을 취소할 수 있다.
  • Member는 팀을 탈퇴할 수 있다.
  • Member는 자신이 팀 가입 요청한 목록을 확인할 수 있다. 

위의 요구 사항을 로직들로 만든 것입니다. 아래의 로직들을 사용하기 위해서는 Entity와 Repository의 쿼리를 작성해주어야 합니다. 하나씩 작성해보겠습니다. 

3. requestTeam() 팀에게 가입신청을 하는 로직

@Override
public Long requestTeam(Long teamId, Long memberId) {
    Team team = teamRepository.findById(teamId).get();
    User user = userRepository.findById(memberId).get();
    RequestTeam requestTeam = RequestTeam.createRequestTeam(user, team);

    if(user.getTeam()!=null){
        throw new AlreadyTeamException("이미 팀을 가지고 있습니다.");
    }
    requestTeamRepository.findByUserId(memberId).ifPresent(
            a -> {
                throw new DuplicateRequestToTeamException("이미 신청한 기록이 있습니다.");
            }
    );

    return requestTeamRepository.save(requestTeam).getId();
}
  • TeamRepository.findById()를 이용하여 신청할 팀이 있는지 확인합니다. findById() 쿼리를 만들지 않았지만 기본적으로 JPA에서 제공해주는 함수입니다.
  • 마찬가지로 UserRepository.findById()를 사용하여 어떤 유저가 신청하는지 찾아줍니다.
  • RequestTeam의 생성 메소드를 통해서 Member와 Team의 연관관계를 넣어주고 생성합니다.
  • 만약 Member가 이미 Team을 가지고 있다면 AlreadyTeamException을 발생시킵니다.
  • Team을 가지고 있지 않다면 해당 팀에 가입 신청을 한 적이 있는지 체크해주며 있으면 예외를 터트립니다.
  • 위의 로직에서 예외가 없었다면 RequestTeamRepository에 저장을 해주어 가입 신청을 완료합니다.
이때 RequestTeam과 User는 다:1 관계이며, 지연 로딩이므로 requestTeam에 담긴 User객체는 프록시입니다. User객체가 바로 사용되므로 fetch join을 통해서 user객체를 프록시가 아닌 객체를 바로 담아옵니다.

RequestTeamRepository를 변경해줍니다. 

public interface RequestTeamRepository extends JpaRepository<RequestTeam, Long> {

    @Query("select re from RequestTeam re join fetch re.user where re.user.id= :memberId")
    Optional<RequestTeam> findByUserId(@Param("memberId") Long memberId);
}

 

@Query: 명명법으로 쿼리를 날릴 수 있지만 직접 query작성이 가능합니다. 

  • query 작성법: "select re(별칭) from RequestTeam(Entity) re(별칭) where ~~" 입니다. 일반적으로 query에 쓰이는 having 절, count절 모두 사용 가능합니다. 

 fetch join: 지연 로딩했던 객체를 한 번에 가져오는 역할을 합니다. "다:1" 에서는 사용이 가능하지만 "1:다" 에서는 paging을 할 수 없으므로 사용하지 않습니다. 

  • fetch join query 작성법: "select re(별칭) from RequestTeam(Entity) re(별칭) join fetch re.user( fetch join 대상 객체) where ~~" 입니다. fetch join한 객체에 대한 별칭은 querydsl을 사용한다면 줄 수 있지만 @query절에선 줄 수 없습니다.

@Param: query에 사용될 memberId 파라미터를 이름으로 지정하여 @Query 안에서 사용이 가능하게 해 줍니다.  

 

Optional: 일반 객체가 아닌 Optional로 받는 이유는 null값이 들어갈 수 있는 예외처리를 할 수 있기 때문입니다. 

 

4. withdrawlTeamRequest() Member가 요청한 것을 취소하는 로직

@Override
public void WithdrawalTeamRequest(Long memberId, Long teamId) {
    RequestTeam requestTeam = requestTeamRepository.findByMemberIdAndTeamId(memberId, teamId).get();
    requestTeam.removeRequest();
    requestTeamRepository.deleteById(requestTeam.getId());
}
  • RequestTeamRepository에서 memberId와 teamId로 requestTeam 객체를 찾습니다. 이때 Member와 Team모두 RequestTeam과 1:다 관계이므로 fetch join을 사용합니다.
  • RequestTeam Entity안에 요청을 없애주는 비즈니스 로직을 만들어줍니다.
  • RequestTeamRepository.deleteById()를 이용하여 RequestTeam을 삭제해줍니다.

가장 먼저 RequestTeamRepository.findByMemberIdaAndTeamId를 만들겠습니다.

public interface RequestTeamRepository extends JpaRepository<RequestTeam, Long> {

    @Query("select re from RequestTeam re join fetch re.user where re.user.id= :memberId")
    Optional<RequestTeam> findByUserId(@Param("memberId") Long memberId);

    @Query(" select re from RequestTeam re join fetch re.user join fetch re.team where re.user.id=:memberId and re.team.id=:teamId")
    Optional<RequestTeam> findByMemberIdAndTeamId(@Param("memberId") Long memberId, @Param("teamId") Long teamId);

}
Member와 Team에서의 RequestTeam을 바로 삭제하므로 fetch join을 통하여 바로 가져옵니다. 

 

RequestTeam에 대한 비즈니스 로직을 작성해줍니다.  아래 함수를 RequestTeam Entity 맨 밑에 추가합니다. 

public void removeRequest(){
    user.getRequestTeamList().remove(this);
    team.getRequestTeamList().remove(this);
}

 

해당 RequestTeam과 연관된 User와 Team은 fetch join을 통하여 가져왔으므로 바로 삭제를 할 수 있습니다. 

5. leavingTeam() Member가 팀을 탈퇴하는 로직

@Override
public void leavingTeam(Long memberId) {
    User user = userRepository.findById(memberId).get();
    user.removeTeam();
}
User Entity안에 팀을 삭제하는 removeTeam() 비즈니스 로직을 만들어줍니다. 영속성 콘텍스트에 속해있는 객체들은 dirty checking 통해 변경 사항이 있다면 DB에 반영을 해줍니다. 이렇게 직접 query를 날리지 않고 dirty checking을 사용하여 DB에 반영하는 것이 일반적입니다.

User와 Team Entity 가장 밑에 로직을 추가해줍니다. 

  • User Entity에 추가
public void removeTeam(){
    this.team.decrease(this);
    this.team=null;
}
Team에 속한 User의 감소 호출 로직을 불러준 뒤 Team=null로 바꾸어 아무 곳에 속하지 않은 상태로 만들어줍니다. 위에서 말했듯이 영속성 콘텍스트가 관리하는 개체이므로 DB에도 Team이 null로 반영될 것입니다.
  • Team Entity에 추가
public void decrease(User user){
    nowSize--;
    userList.remove(user);
}
nowSize는 팀에 속해있는 인원의 크기입니다. 따라서 User가 탈퇴했다면 줄여주고 UserList에서 제외시킵니다.

 

6. findRequestList() Member가 가입 신청한 리스트를 찾아줍니다.

@Override
public List<RequestTeamDto> findRequestList(Long memberId) {
    return requestTeamRepository.findByMemberId(memberId, Sort.by(Sort.Direction.DESC,"createdTime"))
            .stream().map(a -> new RequestTeamDto(a.getRequest(),a.getTeam().getName())).collect(Collectors.toList());
}

 

  • RequestTeamRepository에서 memberId를 이용하여 신청한 시간 순서대로 형성된 List<RequestTeam>를 찾고 List<RequestTeamDto>로 변경해줍니다.
  • Sort.by(Sort.Direction.방향, 필드명): DB에서 query를 이용하여 데이터를 정렬하듯이 Sort를 이용하여 정렬된 데이터를 조회할 수 있습니다.

RequestTeamRepository에 findByMemberId를 추가해줍니다.

public interface RequestTeamRepository extends JpaRepository<RequestTeam, Long> {

    @Query("select re from RequestTeam re join fetch re.user where re.user.id= :memberId")
    Optional<RequestTeam> findByUserId(@Param("memberId") Long memberId);

    @Query(" select re from RequestTeam re join fetch re.user join fetch re.team where re.user.id=:memberId and re.team.id=:teamId")
    Optional<RequestTeam> findByMemberIdAndTeamId(@Param("memberId") Long memberId, @Param("teamId") Long teamId);

    @Query("select re from RequestTeam  re join fetch re.user join fetch re.team where re.user.id= :memberId")
    List<RequestTeam> findByMemberId(@Param("memberId") Long memberId, Sort sort);
}

 

7. 기존 코드에서 추가된 것들과 변경 사항들을 확인해보겠습니다. 

  • User 
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private Long id;

    @Column(name = "email")
    private String email;

    @Column(name = "password")
    private String password;

    @Column(name = "user_name")
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

    @OneToOne(fetch = FetchType.LAZY, mappedBy = "user")
    private League league;

    @OneToMany(mappedBy = "user")
    private List<RequestTeam> requestTeamList =new ArrayList<>();

    @Enumerated(EnumType.STRING)
    private ROLE role;

    @Embedded
    private Address address;




    // 연관 관계 메소드
    public void setTeam(Team team){
        this.team=team;
    }
    public void setLeague(League league){
        this.league=league;
    }


    // 생성 메소드
    public static User createUser(String email, String password, String name, Address address, ROLE role){
        User user = new User();
        user.email=email;
        user.role=role;
        user.password=password;
        user.address=address;
        user.name=name;
        return user;
    }

    
    // 비즈니스 로직 추가
    public void removeTeam(){
        this.team.decrease(this);
        this.team=null;
    }
}
  • Team
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Team extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "team_id")
    private Long id;

    @Column(name = "team_name")
    private String name;

    @Column(name = "team_max_size")
    private Integer maxSize;

    @Column(name = "founder_email")
    private String email;

    @Column(name = "team_now_size")
    private Integer nowSize;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "league_id")
    private League league;

    @OneToMany(mappedBy = "team")
    private List<RequestTeam> requestTeamList = new ArrayList<>();

    @OneToMany(mappedBy = "team")
    private List<RequestLeague> requestLeagueList = new ArrayList<>();

    @OneToMany(mappedBy = "team")
    private List<User> userList = new ArrayList<>();


    public void setUser(User user){
        user.setTeam(this);
    }
    public void addTeam(User user){
        userList.add(user);
    }

    public void setLeague(League league){
        this.league=league;
    }

    public static Team createTeam(User user,String name, Integer maxSize){
        Team team = new Team();
        team.name=name;
        team.maxSize=maxSize;
        team.email=user.getEmail();
        team.nowSize=0;
        team.addTeam(user);
        team.setUser(user);
        return team;
    }

    
    // 비즈니스 로직 추가
    public void decrease(User user){
        nowSize--;
        userList.remove(user);
    }

}
  • RequestTeam
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class RequestTeam extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "request_team_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

    @Column(name = "request_team")
    private Boolean request;


    // 연관 관계 메소드
    private void setUser(User user) {
        this.user = user;
    }

    private void setTeam(Team team) {
        this.team = team;
    }

    private void addRequestTeam() {
        user.getRequestTeamList().add(this);
        team.getRequestTeamList().add(this);
    }

    // 생성 메소드
    public static RequestTeam createRequestTeam(User user, Team team) {
        RequestTeam requestTeam = new RequestTeam();
        requestTeam.request = false;
        requestTeam.setTeam(team);
        requestTeam.setUser(user);
        requestTeam.addRequestTeam();
        return requestTeam;
    }

    
    // 비즈니스 로직 추가
    public void removeRequest(){
        user.getRequestTeamList().remove(this);
        team.getRequestTeamList().remove(this);
    }

}
  • RequestTeamRepository
public interface RequestTeamRepository extends JpaRepository<RequestTeam, Long> {

    @Query("select re from RequestTeam re join fetch re.user where re.user.id= :memberId")
    Optional<RequestTeam> findByUserId(@Param("memberId") Long memberId);

    @Query(" select re from RequestTeam re join fetch re.user join fetch re.team where re.user.id=:memberId and re.team.id=:teamId")
    Optional<RequestTeam> findByMemberIdAndTeamId(@Param("memberId") Long memberId, @Param("teamId") Long teamId);

    @Query("select re from RequestTeam  re join fetch re.user join fetch re.team where re.user.id= :memberId")
    List<RequestTeam> findByMemberId(@Param("memberId") Long memberId, Sort sort);
}
  • TeamRepository
public interface TeamRepository extends JpaRepository<Team, Long> {

}
  • MemberService
public interface MemberService {
    Long requestTeam(Long teamId, Long memberId);
    void WithdrawalTeamRequest(Long memberId, Long teamId);
    void leavingTeam(Long memberId);
    List<RequestTeamDto> findRequestList(Long memberId);
}
  • MemberServiceImpl
@Service
@Transactional
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService{
    private final UserRepository userRepository;
    private final TeamRepository teamRepository;
    private final RequestTeamRepository requestTeamRepository;


    @Override
    public Long requestTeam(Long teamId, Long memberId) {
        Team team = teamRepository.findById(teamId).get();
        User user = userRepository.findById(memberId).get();
        RequestTeam requestTeam = RequestTeam.createRequestTeam(user, team);

        if(user.getTeam()!=null){
            throw new AlreadyTeamException("이미 팀을 가지고 있습니다.");
        }
        requestTeamRepository.findByUserId(memberId).ifPresent(
                a -> {
                    throw new DuplicateRequestToTeamException("이미 신청한 기록이 있습니다.");
                }
        );

        return requestTeamRepository.save(requestTeam).getId();
    }

    @Override
    public void WithdrawalTeamRequest(Long memberId, Long teamId) {
        RequestTeam requestTeam = requestTeamRepository.findByMemberIdAndTeamId(memberId, teamId).get();
        requestTeam.removeRequest();
        requestTeamRepository.deleteById(requestTeam.getId());
    }

    @Override
    public void leavingTeam(Long memberId) {
        User user = userRepository.findById(memberId).get();
        user.removeTeam();
    }

    @Override
    public List<RequestTeamDto> findRequestList(Long memberId) {
        return requestTeamRepository.findByMemberId(memberId, Sort.by(Sort.Direction.DESC,"createdTime"))
                .stream().map(a -> new RequestTeamDto(a.getRequest(),a.getTeam().getName())).collect(Collectors.toList());
    }
}

8. 패키지 구성을 살펴보겠습니다. 

9. MemberService에 관한 Test는 Team이 만들어진 후 가능하므로 그 후에 만들겠습니다.

 

지금까지 Member의 요구사항에 관하여 코드를 작성하였습니다. 아래의 방법을 알아봤습니다.

  1. @Query, @Param 사용법
  2. fetch join 사용법
  3. dirty checking을 활용하기 위한 비즈니스 로직 설계

다음은 MemberController를 작성해보겠습니다. 감사합니다

모든 코드들은 아래 링크에서 확인 가능합니다.

https://github.com/rlaehdals/blogProject

 

GitHub - rlaehdals/blogProject

Contribute to rlaehdals/blogProject development by creating an account on GitHub.

github.com