이번주는 미경님과 유리님과 한 조가 되서 다른것 걱정없이 코딩에만 집중할 수 있는 한 주 였다.

비전공자에 제로베이스인 우리 셋이 팀이라 오류가 터질때마다 사실 백지장을 맞들면서 해결해야 했기에 다른 팀들보다 더 힘들었을 수 있었는데, 서로 도와주려하고 우리 나름의 방식대로 코드의 흐름을 이해하려 노력한 한 주라 지금까지 항해99 기간 중에 가장 기억에 남을 주간이 될 것 같다. 같이 팀원이 되어주셔서 너무 감사하다는 말을 미경님과 유리님한테 전하며 마지막 정리하는 글을 작성해 보려 한다. 


1. 회원 탈퇴 인가 부분 (config)

일단 과제2에 구현해야 할 부분은 완성이 되어서 추가로 회원탈퇴 기능 구현을 시도했다.

회원 탈퇴는 토큰값이 유효하거나 관리자만 할 수 있도록 코드를 작성했는데, 다른 회원으로 로그인을 해도 탈퇴를 시킬 수 있는 오류가 발생했다. 이 부분은 미경님의 예리한 눈으로 문제점을 발견했는데 바로 config 클래스의 인가부분에서 아래와 같이 모든 "/api/user/**" URL 이 permit 받았기 때문이다. 참고로 회원 탈퇴의 경로가 "/api/user/delete/{id}" 로 되어있었기 때문에 이런 오류가 발생하였다.

 

 

위의 코드를 아래와 같이 세부적으로 "/api/user/login", "/api/user/signup" 만 permit 되도록 설정해주었더니 오류 해결 !

 

2. Github merge 왜 안되지?

① 상황

우리조는 각자 기능 파트를 맡긴 했지만, ( 유리님 - 댓글 CRUD , 미경님 - 게시글 CRUD / 예외처리,  나 - 댓글/게시글 좋아요 + 회원탈퇴 ) 어쨌든 모든 기능을 각장 구현해보고 싶은 마음에 각자의 파일로 작업을 하고 있었다. 과제제출 마지막날 부랴부랴 깃헙을 합치려고 했더니 세명의 파일이 모두 달라서 머지를 하면 다른 사람의 파일이 원래 올려진 파일을 통째로 덮어버리는 상황이 발생했다. 

 

② 문제 발생 이유

깃헙에 대한 지식이 무지했기 때문에, 먼저 root 파일을 만들고 그걸 clone받은 후에 작업을 해야 merge 하는 과정에서 한 파일이 될 수 있다는 것을 몰랐다.

 

③ 해결 방법

일단 다시 처음부터 root 파일을 만들고 시작하기엔 시간이 너무 없었기 때문에 일단 내가 만든 전체 파일을 미경님이 clone 받고, 거기에 미경님이 예외처리 부분을 다시 덧붙여서 업로드 하는 방안으로 제출을 완료했다.

 

이번에 깃헙으로 협업하는 연습까지 했으면 너무 좋았겠지만, 우리는 일단 각자 CRUD를 더 견고히 해보자는 생각으로 코드구현에 더 신경을 썼다. 그래서 이슈를 생성하고 PR을 날리는 그러한 작업은 하지 못한 아쉬움이 있다. 다음 프로젝트에는 깃헙으로 협업하는 연습을 해야겠다고 생각 

 

3. 추가로 수정해야할 부분 (시간상 해결 못함)

 

 

반환타입을 아래와 같이 ResponseEntity<MsgResponseDto>로 통일했더니 위와 같이 모든 게시글에 mgs : "성공"이 반환된다. 설계대로라면 이 메시지가 모든 게시글에 뜰게 아니라 게시글 리스트 상위에 위치 해야한다.

 

기술 매니저님이 주신 해결방안 키워드 : ApiResult

다음 프로젝트때 사용해 보는 걸로 ! 

 

4. 마지막 받은 피드백

① Restful API

기술매니저님이 API 명세서를 보시고, 더  Restful 한 API 를 위해 URL 부분을 점층적으로 다시 작성해 보도록 제안해 주셨다.

예를들어, 이전에 작성된 게시판과 댓글의 URL 은 "/api/posts/{id}", "/api/commnets/{id}" 이런 식이었는데, 이 분에서 해당 게시글에 달린 댓글의 경우 "/api/posts/{postId}/comments/{commnetsId}" 이런식으로 계단식/ 점층적인 방법으로 설계하라고 피드백을 주셨다. 추가로 동사를 지양하고 복수형 명사로 작성해야 된다고도 알려주셨다. 지난번에 공부해서 알고는 있었는데, 회원 탈퇴의 경우 어떻게 작명을 해야할지 고민이 되긴 했다.

 

② common package

전역적으로 사용되는 파일들 (예를 들면, 예외처리, Jwt, ApiResult 등) 은 common 이라는 package를 만들어서 사용하기. 

 

그리하여 수정된 사항은 아래 노션 링크 참조 ! 

 

과제 노션 링크 : https://www.notion.so/10-df14faf0f7ed43d2ae1d41d478b8b5f3

 

5. 프로젝트를 마치며

지난 3주를 돌이켜보면, Spring 에 대한 벽이 너무나 높게만 느껴지고 내가 과연 할 수 있을까라는 생각이 오조오억번은 들었던 주특기 주차였다. 아무래도 점점더 실력차도 실감하게 되고, 내가 직접 강의를 보고 이해하기보다 다른 사람의 코드를 보고 재작성 하다보니 자괴감이 들기도하고, 만약 현업에서 이런 참고 레퍼런스가 없다면 내가 스스로 작성할 수 있을까? 라는 생각도 드는 마음이 복잡한 한 주 였다. 

 

그래서 일단 남의 코드를 보고 작성하더라도 완성된 코드를 계속 보면서 어떤식으로 돌아가는지 이해하려고 했고, 첫주에 봐도 모르겠다는 생각이 그래도 이제는 내 코드를 가지고 응용은 할 수 있겠다라는 상태로 바뀌게 되었다. 아무래도 이 3주가 나와 같이 기본기가 없는 사람들에겐 많이 힘들었던 기간이었던 것 같다. 그래서 무려 5명의 스프링 반 사람들이 하차를 하기도 했다. 

 

아직 많이 부족하지만, 다음주 부터 시작되는 프로젝트에서 팀에 피해없이 1인분을 할 수 있도록 열심히 해야겠다.

 

[목차]

 

1. Response 에 null 값이 뜨는 문제

2. 순환참조 오류

3. cascade 연속성 전이

4. @Transactional 

+ 이번 과제 관련 추가 (발생 가능성이 있었던 문제)

 


1. Response 에 null 값이 뜨는 문제

 

 

1. 이슈

위와 같이 게시글을 입력하고 조회했을 때, 수정시간과 작성시간의 값이 null로 뜨는 문제가 발생했다.

 

2. 발생 이유 

위는 ResponseDto 클래스 생성자 부분이다. 

null값이 뜰 때는 주석처리되어 있는 부분을 작성하지 않은 상태였기 때문에 생성자 초기화가 되어있지 않은 상태인게 문제의 이유였다.

 

3. 해결 

주석처리 했던 부분을 기입했더니 아래와 같이 값이 잘 들어오는 것을 확인할 수 있었다.

 

 

 

  • 같은 조의 미경님도 PostMan으로 게시글 조회부분을 확인하실 때 id값이 들어오지 않는 문제가 있으셨는데, 나와 같은 이유로 Dto부분 생성자 부분에 id 값을 넣어주지 않아서 발생한 문제였다. 
  • 처음에 스프링에 대한 이해가 부족하다 보니, Entity와 Dto 두 가지 차이에 대한 이해가 부족해서 발생한 문제였다.
  • Entity는 DB에 저장되어지는 테이블 값이라고 생각하면 되고, Dto는 내가 원하는 값만 cutomize 해서 request를 받거나 response를 주는 부분이라고 생각하면 쉬울 것 같다. 
  • 따라서 Entity 처럼 모든 항목을 기입할 필요 없이, 게시글을 받는 부분의  RequestDto 부분의 생성자 부분엔 유저로 부터 입력값을 받게될 username, title, content 부분만 넣어주면 되고, 또 화면단으로 반환되는 부분은 ResponseDto 생성자 부분에 password를 제외한 id, username, title, content, modifiedAt, createdAt 등의 값들을 생성자 부분에 넣어주면 된다.

 

2. 순환참조 오류

 

 

1. 이슈 

Post와 Comment를  각각 @OneToMany, @MantToOne 로 양방향 연관관계를 맺어주고 PostMan에서 게시글 조회를 했더니 순환참조오류인 Stack over flow 문제가 발생했다.

보이는 바와 같이 18,562 줄이나 조회가 되었음을 볼 수 있다.

 

Comment Entity
Post Entity

위와 같이 Post Entity 와 Comment Entity 부분에 양방향으로 연관관계가 맺어져 있다.

Comment Entity

2. 발생이유

그리고 위와 같이 Comment Entity 자체에 post 값이 들어가 있는데,

게시글을 조회하는 과정에서 그 밑에 연관관계로 맺어진 Comment List가 딸려오고, 

그 딸려온 Comment 안에 또 모든 post값이 들어가기 때문에 돌고 돌아버리는 순환참조 오류가 발생한 것이다.

 

 

3. 해결 방법 (2 가지)

3-1. @JsonIgnore을 사용한다.

Post Entity 부분

  • 당장의 순환참조 오류는 해결했지만, 완전한 방법은 아니다.
  • 임시방편이라고 할 수있다.

 

3-2. Dto부분의 설계를 잘 한다.

PostResponseDto 부분

  • List<> 안의 제네릭 부분을 Comment 엔티티로 하지 말고 Dto로 변환한뒤, 다시 배열을 만들어 Comment(Response)Dto 값들을 넣어준다.
  • 배열을 변환해주는 과정은 Dto 혹은 Service단 모두 가능하지만, 일단 아래와 같이 Service 단에서 실행해 주었다.

 

PostService 게시물 전체조회 부분

  • 이중 for each 반복문으로 CommentDto의 값이 들어간 commentList를 만들어 주고, 다시 그 댓글 리스트를 게시글 리스트에 넣어주었다. 

해결후 제대로 Response 값이 찍힘

 

  • 양방향 연관관계를 맺을 땐 항상 순환참조 오류를 조심해야 한다.
  • 이를 방지하기 위해 Dto설계를 견고히 하자.

[무한 순환참조 참고자료] : https://subji.github.io/posts/2020/08/06/infiniterecusionofjpa

 

3. cascade 연속성 전이

PostMan 게시글 삭제 오류
콘솔창 에러 메세지

1. 이슈

게시글과 댓글이 일대다 양방향 연관관계로 매핑되어 있는 상태에서 댓글을 삭제할 땐 오류가 없지만, 게시글을 삭제할 경우 오류가 발생

 

2. 발생이유 : 영속성 전이 문제

 

영속성 전이란?

부모 엔티티가 영속화될 때 자식 엔티티도 같이 영속화되고, 부모 엔티티가 삭제될 때 자식 엔티티도 삭제되는 등 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 전이되는 것을 의미

즉, 특정 엔티티에 대해 특정한 작업을 수행하면 관련된 엔티티에도 동일한 작업을 수행한다는 의미입니다.

 

CascadeType는 코드와 같이 PERSIST, MERGE, DETACH, PREFRESH, REMOVE, ALL 로 구성되어 있다.

각각의 operations에 대한 설명은 다음과 같다.

  • CascadeType.ALL: 모든 Cascade를 적용
  • CascadeType.PERSIST: 엔티티를 영속화할 때, 연관된 엔티티도 함께 유지
  • CascadeType.MERGE: 엔티티 상태를 병합(Merge)할 때, 연관된 엔티티도 모두 병합
  • CascadeType.REMOVE: 엔티티를 제거할 때, 연관된 엔티티도 모두 제거
  • CascadeType.DETACH: 부모 엔티티를 detach() 수행하면, 연관 엔티티도 detach()상태가 되어 변경 사항 반영 X
  • CascadeType.REFRESH: 상위 엔티티를 새로고침(Refresh)할 때, 연관된 엔티티도 모두 새로고침

2. 해결 방법

  • cascade = CascadeType.REMOVE 를 코드를 추가 !

 

  • 코드 추가 후 게시글이 잘 삭제되는 것을 확인 !

[영속성 전이 참고 자료] : https://zzang9ha.tistory.com/350

 

4. @Transactional 

1. 이슈

게시글 댓글 수정이 PostMan response 부분에 문제없이 처리가 되었다고 뜨는데, h2 콘솔 데이터베이스를 확인하면 데이터 변환이 일어나지 않음.

 

Comment Service 파일

 

PostMan에는 수정사항이 잘 반영되었다.
h2-console

그러나 h2-console 에는 수정이 반영되지 않았다.

 

2. 발생 이유

update 메소드에 @Transattilnal 어노테이션이 빠졌다.

Comment Service update 부분
수정 후 h2-console

3. 해결방법 : update 메소드에 @Transattilnal 어노테이션 추가

위와 같이 정상적으로 업데이트가 되는 것을 확인 할 수 있다. -> 이부분은 스프링 트랜스잭션 개념으로 다시 정리 할 것 !  :) 

 

+ 이번 과제 관련 추가 (발생 가능성이 있었던 문제)

① N+1 문제

 

연관 관계에서 발생하는 이슈로 연관 관계가 설정된 엔티티를 조회할 경우에 조회된 데이터 갯수(n) 만큼 연관관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오게 된다. 이를 N+1 문제라고 한다.

  • N+1은 JPA를 사용하면서 연관관계를 맺는 엔티티를 사용한다면 한번 쯤은 부딪힐 수 있는 문제이다.
  • Fetch Join이나 EntityGraph를 사용한다면 Join문을 이용하여 하나의 쿼리로 해결할 수 있지만 중복 데이터 관리가 필요하고 FetchType을 어떻게 사용할지에 따라 달라질 수 있다.
  • SUBSELECT는 두번의 쿼리로 실행되지만 FethType을 EAGER로 설정해두어야 한다는 단점이 있다.
  • BatchSize는 연관관계의 데이터 사이즈를 정확하게 알 수 있다면 최적화할 수 있는 size를 구할 수 있겠지만 사실상 연관 관계 데이터의 최적화 데이터 사이즈를 알기는 쉽지 않다.
  • JPA 만으로는 실제 비즈니스 로직을 모두 구현하기 부족할 수 있다. JPA는 만능이 아니다. 간단한 구현은 JPA를 사용하여 프로젝트의 퍼포먼스를 향상 시킬수 있겠지만 다양한 비즈니스 로직을 복잡한 쿼리를 통해서 구현하다보면 다양한 난관에 부딪힐 수 있다. 그리고 불필요한 쿼리도 항상 조심해야 한다. 그러므로 QueryBuilder를 함께 사용하는 것을 추천한다. 그러면 생각보다 다양한 이슈를 큰 고민없이 바로 해결할 수 있다.

[참고 자료] https://incheol-jung.gitbook.io/docs/q-and-a/spring/n+1 

 

② @JoinColunm이 꼭 필요한가 ?

 

다대일 연관관계에서 @JoinColumn(name) 옵션은 컬럼 이름 매핑에 사용되는 어노테이션이지, 연관관계에는 아무런 영향이 없다. 단, 조인 대상 컬럼을 변경하기 위해 @JoinColumn(referencedColumnName) 어노테이션을 활용할 수 있다! 그러니 무조건적으로 @JoinColumn을 생략해선 안되겠다.

 

다대일 연관관계 매핑에서만 그렇다. 일대다 단방향 연관관계 매핑에서는 @JoinColumn 어노테이션을 꼭 명시해주어야 한다. 그렇지 않을 경우 중간 테이블을 자동으로 생성한다.
또, 일대다 양방향 연관관계를 매핑하는 방식에서도 연관관계의 주인을 일(1)에게 주려 하는 경우 다(N) 쪽에 @JoinColumn(insertable = false, updatable = false)를 사용하면서 연관관계의 주인을 강제시키는 방법으로도 사용할 수 있다.

즉, 정리하자면 @JoinColumn 어노테이션은 원래 여러가지 옵션을 제공하고 할 수 있는 일이 많다. 그러나 다대일 연관관계 매핑에서는 JPA가 기본적으로 취하는 전략들이 있기 때문에, @JoinColumn 어노테이션을 생략해도 기대하는 대로 동작한다.

 

 

[참고 자료] : https://hyeon9mak.github.io/omit-join-column-when-using-many-to-one/

 

게시글 작성 프로젝트 과제를 3주동안 점층적으로 수행했다. 과제를 마무리하며 모든 부분의 코드를 정리할 순 없고, 마지막 주차때 알게된 내 기준 기억하고 싶은 코드의 일부들을 정리해 보려 한다. 정리해보니 3주 동안 정말 많은 기능을 배운 것 같다. 비록 직접 작성한 코드는 적고, 다른 사람들의 코드를 참조해서 짜집기 하거나 그대로 복사하기도 했지만, 흐름을 대략적으로 이해하는데 의의를 두려고 한다.

 

 

주특기 1주차 : Spring CRUD 구현

  • 게시물작성, 조회, 선택조회, 수정, 삭제 구현하기. 반환값을 Entity가 아니라 Dto로 감싸서 주기.  

주특기 2주차 : JWT토큰 이용하여 회원가입, 로그인 구현

  • 토큰값이 유효한 작성자 혹은 Admin권한이 있는 작성자만 게시글 수정 + 삭제 가능하게 하기.
  • 게시글에 댓글을 추가하고 댓글 작성, 수정, 삭제 구현하기 (연관관계 테이블 매핑)
  • 게시글이나 댓글 삭제 상태코드 Httpstaus 형식으로 반환하기.
  • 예외처리 커스텀해서 반환하기.

주특기 1주차 : Spring Security 이용 + 댓글과 게시글에 좋아요 추가

  • 2주차에 이어 JWT토큰 인증코드 대신에 Spring Security 사용하여 코드 간소화 시키기.
  • 게시글이나 댓글에 좋아요 기능을 추가하고 한번 더 누르면 취소하는 기능까지 구현하기.
  • 좋아요 숫자를 화면단에 같이 반환해주기.

1. CascadeType.REMOVE

Post Entity 부분

  • 게시글이 삭제되면 게시글에 달린 댓글도 동시에 삭제되어야 한다.
  • 그럴때 사용되는 방법 : cascade = CascadeType.REMOVE

 

2. 순환참조를 막는 방법

Comment Entity 생성자 부분

  • 위의 Comment 엔티티 부분에 post 객체를 갖고있다.
  • post(게시글)을 반환할 때 comment (댓글)도 같이 반환하는데, 게시글 안의 댓글에 또 다시 게시글이 포함되어 있으므로 양방향으로 연관관계가 형성되어있는 게시글과 댓글에 순환참조가 일어나게 된다.
  • 내가 배운 해결방법은 아래 두 가지 이다.

 

1. @JsonIgnore을 사용한다.

Post Entity 부분

  • 당장의 순환참조 오류는 해결했지만, 완전한 방법은 아니다.
  • 임시방편이라고 할 수있다.

 

2. Dto부분의 설계를 잘 한다.

PostResponseDto 부분

  • List<> 안의 제네릭 부분을 Comment 엔티티로 하지 말고 Dto로 변환한뒤, 다시 배열을 만들어 Comment(Response)Dto 값들을 넣어준다.
  • 배열을 변환해주는 과정은 Dto 혹은 Service단 모두 가능하지만, 일단 아래와 같이 Service 단에서 실행해 주었다.

 

PostService 게시물 전체조회 부분

  • 이중 for each 반복문으로 CommentDto의 값이 들어간 commentList를 만들어 주고, 다시 그 댓글 리스트를 게시글 리스트에 넣어주었다.
  • 이 부분은 잘 기억해 뒀다가 다음 프로젝트때도 써먹어야겠다 

 

 

3. Spring Security 사용해서 인증된 유저로 게시글 작성하기

 

PostController

  • 클래스 위에 @RestController를 사용하여 스프링빈에 등록해주고, @RequiredArgsConstructor 어노테이션으로 아래 postService의 생성자를 만들어 주었다.
  • 필드부분에  PostService 의존성 주입을 해주었다.
  • createPost라는 클래스 매개변수로 @AuthenticationPrincipal 어노테이션을 사용하여 스프링시큐리티로 토큰이 인증된 User를 UserDetailsImpl에서 가져올 수 있도록 하였다. 

 

UserDetailsImpl 부분

  • UserDetailsImpl 부분이다. 주석으로 처리한 설명과 같이 여기엔 토큰값이 검증된 user가 담겨 있다.

 

PostService 부분

  • 비지니스 로직을 담당하는 PostService 단의 모습이다
  • 클래스 가장 윗부분에 @Service, @RequiredArgsConstructor를 컨트롤러 단과 같은 이유로 달아주었다.
  • 그리고 마찬가지로 필드부분에 postRepository 의존성 주입을 해주었다.
  • createPost부분에 매개변수로 PostRequestDto 와 UserDetailsImpl의 User를 받아와 postRepository에 .save() 함수로 저장하는 로직을 작성하고, 반환으로 new PostResponseDto(post)를 주었다.

 

4. 게시글 좋아요 구현 부분

PostLikeRepository 부분

  • 데이터베이스에서 데이터를 꺼내오는 repository 부분 이다.
  • 참조값을 userId 와 해당 postId로 설정하고 하나는 find를 사용, 다른 하나는 delete를 사용한 쿼리문을 만들어 주었다.

PostLikeService 부분

  • PostLikeService 부분이다.
  • 일단 가장 윗 부분에서 post id로 조회를 했을 때, 해당 게시글이 있는지 확인하는 부분을 작성해 주었고, 게시글이 없을 경우 예외처리도 작성해 주었다.
  • 아랫 부분에 if문을 사용하여, 만약 조회한 postLikeRepository가 비어있다면 좋아요를 .save() 메소드를 통해 저장해주고 반환값으로 HttpStatus와 String 타입의 메세지 "좋아요 성공"을 주었다.
  • else문을 통해 좋아요가 눌러있지 않는 경우 delete 쿼리문을 이용해 해당 좋아요를 데이터베이스에서 삭제해 주는 코드를 작성해 주었다. 반환값은 마찬가지로 HttpStatus와 String 타입의 메세지 "좋아요 취소"로 적어주었다.

 

MsgResponseDto 부분

  • 위의 반환타입을 MsgResponseDto로 정해주었는데 그 형태는 위와 같다.

서비스 요구사항 

 

 

ERD 설계

 

 

① Entity 테이블 항목

  • Post (게시글)
  • User (사용자)
  • Comment (댓글)
  • CommentLike(댓글 좋아요)
  • PostLike (게시글 좋아요)
  • Timestampled (작성시간, 수정시간 보여주는 상위클래스)

 

② 테이블간 연관관계

  • Post : User = N : 1
  • Comment : User = N : 1
  • Post : Comment = 1 : N
  • Post : PostLike = 1 : N
  • Comment : CommentLike = 1 : N
  • User : CommentLike = 1 : N
  • User : PostLike = 1 :N

※ 내가 작성한 코드는 연관관계가 양방향으로 설계되었는데, 기술매니저님이 단방향으로도 한번 구현해보라고 하셨다.

 

 

③ 실제 구현된 ERD 

 

 

API 설계

 

+ Recent posts