JAVA/소박한그룹 프로젝트

[211009] 회원이 작성한 게시글 목록을 JPA 1:N 양방향 매핑으로 구현해보자

kth990303 2021. 10. 9. 12:52
반응형

사실 이 작업은 댓글 기능까지 어느정도 구현한 후에 진행해보려 했는데,

댓글-게시글, 댓글-회원 테이블에 양방향 연관관계 매핑이 필요할 것 같아 연습 겸 이 작업 먼저 해보았다.

https://github.com/kth990303/BOJStudyList/issues/20

 

회원이 작성한 게시글 수를 볼 수 있게 해주세요 · Issue #20 · kth990303/BOJStudyList

양방향 매핑으로 수정하면 될 듯? 그 전에 연관관계도 다시 공부하면 좋을 것 같습니다.

github.com

 

그동안 단방향 매핑 코드만 짜다가, 오랜만에 양방향을 적용하려 하니까 가물가물해서 헤맸다.

오늘 한 작업은 아래와 같다.

  • Member - Post 양방향으로 수정
  • 회원이 작성한 게시글 목록 메소드 생성
  • Test Code 작성

어떻게 진행했는지 포스팅해보겠다.


Settings

  • Spring Boot
  • Hibernate
  • MySQL, JPA
  • MapStruct
  • Spring Security

Domain

우선 Member와 Post는 1:N 연관관계 단방향으로 이루어져있었다.

이를 양방향으로 바꾸려는 것이므로 Member 도메인을 아래와 같이 수정해주자.

 

Member.class

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="member_id")
    private Long id;
    @Column(unique=true)
    private String name;
    private String password;
    private String email;
    private String tier;

    @OneToMany(mappedBy = "member")
    private List<Post> posts=new ArrayList<>();

    // update Entity
    public void updateMember(final Post post){
        posts.add(post);
    }
    
    // Another Code ...
}

@OneToMany를 추가해주었고, @OneToMany는 기본이 LAZY 지연로딩을 지켜주므로 fetch를 건드릴 필욘 없다.

그리고 위의 update Entity 주석 밑 코드가 중요한데, 이 코드가 없으면 List<Post> post가 따로 업데이트가 되지 않으므로 주의하자. 만약 이 코드가 없으면 id는 null이면 안된다는 이상한 에러가 발생할 것이다.

 

Post.class

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Post {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "post_id")
    private Long id;
    private String title;
    @Column(columnDefinition = "LONGTEXT")
    private String contents;
    @Embedded
    private PostPeriod postPeriod;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;
    private Long views;

    public Post(String title, String contents, Member member, PostPeriod postPeriod) {
        this.title=title;
        this.contents=contents;
        this.member=member;
        this.postPeriod=postPeriod;
        this.member.updateMember(this);
    }

    // update Entity
    public void updatePost(String title, String contents, PostPeriod postPeriod){
        this.title=title;
        this.contents=contents;
        this.postPeriod=postPeriod;
        this.member.updateMember(this);
    }
    
    // Another Code ...
}

this.member.updateMember(this)에 주목하라.

아까 Member.class에서 updateMember 메소드를 만들어준 것을 사용한다.

여기서 updateMember(this)의 this는 Post를 의미하며,

update 코드에서 update를 마친 후에 마지막으로 Member의 postList에 추가해주는 작업을 거친 것이다.


Service

그럼 이제 회원이 작성한 게시글 목록을 만들어주는 Service 로직을 만들어줄 차례이다.

원래 있었던 MemberService 클래스에 만들어주려다가,

MemberService에는 회원 관련된 로직에만 집중할 수 있도록 하고 싶어 따로 MemberPostService.class를 생성해주었다. 두 개 이상 @Service 어노테이션을 사용해도 상관없다고 한다. 오히려 재사용성, 가독성이 높아져 좋은 코드가 될 수 있다고 한다.

 

MemberPostService.class

@Slf4j
@Service
@Transactional
@Getter
@Setter
@RequiredArgsConstructor
public class MemberPostService {
    private final MemberRepository memberRepository;
    private final PostIdMapper postIdMapper= Mappers.getMapper(PostIdMapper.class);

    // 회원이 작성한 게시글 목록
    public List<PostIdDto> findPostsByMember(Long memberId){
        Member member = memberRepository.findById(memberId).get();
        List<Post> posts = member.getPosts();
        return postIdMapper.toDtoList(posts);
    }
}

method 이름은 findPostsByMember가 적절할 듯하다. 슬슬 메소드이름 짜는 작업이 힘들어지고 있다.

나중에 기능이 추가되고 테이블이 많아지면 더더욱 힘들 듯...

 

여기서 PostIdMapper이 무엇인지 궁금해할 수 있는데, 나는 mapstruct로 DTO <-> Entity 변환작업을 거쳐줘서 그렇다.

mapstruct로 dto<->entity 과정을 거치는 과정이 궁금하다면 아래 포스팅을 참고하자.

https://kth990303.tistory.com/131

 

[Spring] MapStruct를 이용한 Entity, Dto 반환 및 고찰

그 동안 View layer에서 Entity에 직접적으로 접근하도록 코드를 짰던 나에게, 이번 DTO 적용은 상당히 고된 일이었다. 사실 dto는 단순한 entity의 클론 느낌이라 적용이 크게 어렵지 않을 줄 알았는데,

kth990303.tistory.com

https://kth990303.tistory.com/139

 

[Spring] MapStruct 수동으로 오버라이딩 코드 구현하기

Entity dto 반환 mapper를 알아보던 중, ModelMapper보다 MapStruct가 성능이슈가 적고, 편리하다는 말을 들어 mapstruct를 애용중이다. 간단한 mapstruct 이용법은 아래 포스팅에서 볼 수 있다. https://kth99030..

kth990303.tistory.com


Controller

 

사실 컨트롤러는 크게 수정할 것이 없었다.

원래 있었던 컨트롤러 코드에 service 로직을 이용하는 코드를 추가해주면 됐기 때문이다.

나는 "/user/{name}" URI 주소에 해당하는 매핑에 넣었다.


Test Code

@Slf4j
@SpringBootTest
@Transactional
@Rollback(false)
class MemberPostServiceTest {
    @Autowired
    MemberService service;
    @Autowired
    PostService postService;
    @Autowired
    MemberPostService memberPostService;
    @Autowired
    PostMapper postMapper;

    public void createMember() {
        Member member=new Member("test", "Bronze V", "test@naver.com", "123456");
        Member member2=new Member("test2", "Bronze V", "test2@naver.com", "1234");
        Member member3=new Member("test3", "Platinum V", "test@naver.com", "123456");

        service.join(service.toDto(member));
        service.join(service.toDto(member2));
        service.join(service.toDto(member3));
    }
    @Test
    void findPostsByMember() {
        createMember();

        MemberDto memberDto = service.findByName("test");
        Long memberId = service.findIdByName(memberDto.getName());

        PostDto postDto1=new PostDto("new Post1", "hello");
        PostDto postDto2=new PostDto("new Post2", "world");

	// memberName이 "test"인 멤버가 postDto1, postDto2 게시글 작성
        postService.enroll(postDto1, "test");
        postService.enroll(postDto2, "test");

	// test 멤버가 작성한 게시글이 총 2개로 잘 들어가 있는지 assert
        List<PostIdDto> postsByMember = memberPostService.findPostsByMember(memberId);
        Assertions.assertThat(postsByMember.size()).isEqualTo(2);
    }
}

위 테스트코드를 작성해서 테스트해보았다.

참고로 Rollback(false)는 없어도 되는 코드이다. Rollback(false)로 두면 테스트한 이후에 DB가 리셋되지 않고 그대로 남아있는데, 난 포스팅에 db 테이블을 기록하기 위해 Rollback(false)로 해둔 것 뿐이다.

Test Passed

참고로 postService.enroll 메소드는 멤버명이 test인 유저가 postDto 게시글을 등록하게 하는 코드이다.

Controller에서 Spring Security 코드로 로그인한 멤버가 누구인지 찾아내는 작업을 해준다. 원래는 Service에 있었는데, 그렇게하면 Service Test도 Mock을 이용해야 할 듯해서 MockTest는 Controller에서만 할 수 있도록 해주기 위해 수정하였다.

 

멤버명이 중복일 수도 있지 않냐고 할 수 있는데, 여기서 멤버명은 백준 닉네임으로 하는 것을 원칙으로 두기 때문에 중복이 될 수 없기 때문에 안심해도 된다. (백준 규정상 백준닉네임은 중복 불가능)

 

DB에도 잘 저장돼있음을 확인할 수 있다.

 

DB Query:

- select * from member;

- select * from post;

- select m.member_id, m.name, p.title, p.contents from member m inner join post p on m.member_id=p.member_id;


View (Thymeleaf 혹은 그 외 프론트엔드)

아직 view 코드는 작성하지 않았다.

애초에 이번 포스팅은 로직을 기록해두기 위한 포스팅이라 View 코드를 작성하지 않고 포스팅하였다.

즉, 아직 할 일은 남아있다는 점... ㅜㅜ


이렇게 양방향 매핑을 활용한 코드를 짜보았다.

오늘도 하나의 이슈를 (완벽하게는 아니지만) 처리했다.

 

다만, 아직 성능처리까지 신경쓴 코드가 아니기 때문에 이후에 수정할 점은 존재할 수 있다. (페이징, db 설계 등)

 

이 프로젝트의 깃헙주소는 아래와 같다.

https://github.com/kth990303/BOJStudyList/

 

GitHub - kth990303/BOJStudyList: BOJ 그룹스터디 회원들의 공간을 만들자~

BOJ 그룹스터디 회원들의 공간을 만들자~. Contribute to kth990303/BOJStudyList development by creating an account on GitHub.

github.com

 

반응형