LostCatBox

SpringProject-Board-CH03

Word count: 1.4kReading time: 8 min
2022/12/24 Share

Spring 게시판 프로젝트3편 (게시판 작성자만 수정,삭제)

Created Time: July 14, 2022 8:39 AM
Last Edited Time: August 3, 2022 11:07 AM
References: https://dev-coco.tistory.com/120?category=1032063
Tags: Java, Spring, Computer

Spring 게시판 프로젝트

  • 현재 user는 email이 unique=true로 검증하여 로그인 처리함
  • 로그인 주소는 /login
  • 권한은 그냥 기본적인 ROLE_USER, ROLE_ADMIN으로 사용
  • post list 주소는 /basic/post/view

실수한것

  • final 안붙이고 @RequiredArgsConstructor 사용함…>>안됨 제발..그니까 해당 객체 사용시 nullpointExeption 발생
  • Post Entity와 Post Dto에 user 단방향 필드가 추가되었는데,내가 getpost()라는 서비스 함수에서 따로 함수를 안빼고 직접 엔티티를 DTO로 build()하고있었으므로, user(user)가 빠져있었음. 그래서 계속 null뜸..
  • thymeleaf 뷰에서 관계가 있는 comment.getUser().getNickname()을 할려고했지만, 불가능했다. 따라서 commentDto에서 build시에 nickname 를 따로 더 넣어줫다. 해결
  • thymeleaf에서 @{}안에 ${}쓸일있었ㄴ느데 이때는 __${}__

왜 PostResponseDto 를 따로 두는지?

  • PostDto에 댓글 리스트 까지 모두 저장하면, 불필요한 정보 및 실시간 정보가 DB에 저장되며, 구지 이렇게 할필요없이. postget()이 응답을 해주기 위해 필요한 경우에만 Dto에서 댓글과의 관계를 가져와서 list로 반환해서 채워준 객체를 응답으로 해주면, 간단해지고, 불필요한 비용이없어지므로

  • domain( DAO) 파일들에다가 왜 update()로직을 넣어놓는것일까?

    • 다음과 같이 update()로직이 없다면, 항상 일정한 부분을 정확히 업데이트해야하는 상황이지만 어느 부분을

      1
      2
      3
      4
      5
      6
      public Long editPost(Long id, PostDto postDto) {
      //회원을 확인하기위해 다시 디비조회?
      Post post = postRepository.findById(id).get();
      postDto.setUser(post.getUser());
      return postRepository.save(postDto.toEntity()).getId();
      }
    • 하지만 DAO에 update()로직을 넣어준다면 간결하게 해당하는 id를 가진 인스턴스를 가져와 해당 내용만 update 해준다면 매우간단해진다.

    1
    2
    3
    4
    5
    6
    @Transactional
    public void update(Long commentId, CommentDto commentDto){
    Comment comment = commentRepository.findById(commentId).orElseThrow(() ->
    new IllegalArgumentException("해당댓글이 존재하지 않습니다"));
    comment.update(commentDto.getComment());
    }

아주 좋은 참고자료 JPA 더티체킹은 save()를 대체하지않는다

https://github.com/jojoldu/freelec-springboot2-webservice/issues/47

  • JPA더티체킹은 트랜잭션이 끝나는 시점에 변화가 있는 모든 엔티티 객체를 DB에 자동으로 반영해준다.( 기준은 최초 조회 상태와 비교)
  • JPA에서는 엔티티를 조회하면 해당 엔티티의 조회 상태 그대로 스냅샷 을 만들어놓습니다. 그리고 트랜잭션이 끝나는 시점에는 이 스냅샷과 비교해서 다른점이 있다면 Update Query를 데이터베이스로 전달합니다.
  • 당연히 이런 상태 변경 검사의 대상은 영속성 컨텍스트가 관리하는 엔티티에만적용 됩니다. 아래 두가지 경우만
    • 트랜잭션 범위 내에서 save하거나
    • 트랜잭션 범위 내에서 저장된 엔티티를 조회해온 경우

질문

@transactional에 대해 찾아보던 중 의문이 생겨 질문드립니다!

  1. JPA 엔티티를 업데이트할 때 Dirty Checking을 지원해 트랜잭션 안에서는 save를 명시적으로 호출하지 않아도 commit 시에 판단해서 업데이트해준다고 알고 있습니다.
1
2
3
4
5
@Transactional
public Notice update(Long noticeId, String content) {
Notice notice = noticeRepository.findById(noticeId).get();
notice.setContent(content);
}

위의 코드와 아래의 코드의 차이가 뭔지 궁금합니다. (여러 repository를 접근하는 코드가 있을 시 Transactional을 명시해주지 않으면 해당 접근하는 코드마다 트랜잭션 단위가 설정되나 어노테이션을 선언하면 해당 메소드 내의 명령문을 원자적으로 처리해준다고 이해했는데 맞는 것인지..)

1
2
3
4
5
public Notice update(Long noticeId, String content) {
Notice notice = noticeRepository.findById(noticeId).get();
notice.setContent(content);
noticeRepository.save(notice);
}
  1. 생성 코드에서 save를 하고 있는데도 Transactional을 붙이신 이유가 궁금합니다.
1
2
3
4
@Transactional
public Long save(PostsSaveRequestDto requestDto) {
return postsRepository.save(requestDto.toEntity()).getId();
}
  1. 목록 조회 시 Transactional을 붙이면 조회 속도가 왜 개선되고 트랜잭션 범위를 유지해야 하는 이유는 무엇인지 궁금합니다!
1
2
3
4
5
6
7
@Transactional(readOnly = true)
public PostsResponseDto findById(Long id) {
Posts entity = postsRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("해당 사용자가 없습니다. id=" + id));

return new PostsResponseDto(entity);
}

답변

일단 오해를 하나 푸셔야하는데요 :)JPA의 더티체킹은 save를 대체하지 않습니다update를 대체한다고 보시면 됩니다.

영속성 컨텍스트에 포함된 엔티티들의 변경을 감지하는 것입니다.처음 만들어진 엔티티들은 아직 영속성 컨텍스트 대상에 포함되지 않습니다.

  1. 트랜잭션 범위 내에서 save하거나
  2. 트랜잭션 범위 내에서 저장된 엔티티를 조회해온 경우

에만 해당됩니다.

  1. 두 코드 모두 테이블의 최종 상태는 동일합니다.다만, 객체의 관점에서 난 내 상태를 변경했어에서 변경했지만 디비에도 반영은 또 따로 해줘야한단다 가 2번의 코드입니다.1번은 객체가 자기 할일만 하는 코드로 끝난 상태이고요.그래서 객체지향적인 관점에서 2번은 사실 좋은 형태로 보긴 힘듭니다.
  2. 2번은 트랜잭션이 없을 경우 save하다가 잘못될 경우 롤백이 안되서 그렇습니다save할때 연관관계에 의해 다른 엔티티들도 save/update 되야 한다거나여러 엔티티를 한번에 저장해야한다거나단일 엔티티를 저장하지만 저장후 update가 한번더 이루어져야한다거나등등 다양한 케이스에서 전체 반영되거나 전체 취소되는 상황이 필요합니다. 결국 최종적으로 부분반영을 막기 위해 사용합니다.
  3. 일단은 트랜잭션 어노테이션이 없으면 @OneToMany, @ManyToMany와 같은 레이지 로딩이 필요한 엔티티들이 정상조회되지 않습니다.JPA를 사용하다보면 부모-자식 관계 (@OneToMany)를 많이 사용하는데이 옵션이 트랜잭션이 없으면 정상작동 하지 않습니다.LazyInitializationException 가 바로 그런 경우입니다.그래서 OneToMany나 ManyToMany와 같이 레이지 로딩을 지원하면서 롤백 기능이 없어 성능 향상이 어느정도 되어있는 readOnly 옵션을 사용한 것입니다.

개요

  • post user Post와 User 단방향 관계 구현(relation에 대해서는 다음 포스트를 참고하자 (spring 관계 구현))
    • Post와 User는 M:1 관계이므로, 외래키는 Post가 같는다. 따라서 Post는 주인 @JoinColum 사용, 주인이 아닌 User에서 Post와 관계를 맺는다면 mappedBy=를 사용하면된다.
  • 로그인 성공시 session에 user에 대한 정보를 저장
  • 포스트 조회시 조회 작성자 본인인지 확인후 맞다면, model에 writer=true로 하여 타임리프에서 수정 삭제 없앰(api에서의 백엔드 권한체크X)

구현

Post

1
2
3
4
5
6
public class Post { 
/...
@ManyToOne(fetch =FetchType.LAZY)
@JoinColumn(name="user_id")
private User user;
/...
  • Post입장에서는 ManyToOne이며, 다대일관계이다.
  • 외래키를 매핑하기위해, User엔티티의 id필드를 “user_id”라는 이름으로 외래키 갖게한다.

@OneTomany의 기본 Fetch 전략은 LAZY(지연로딩)이며, @ManyToOne의 기본 Fetch전략은 EAGER(즉시 로딩)이다. EAGER전략 사용시 필요하지 않은 쿼리도 JPA에서 함께 조회하기 때문에 N+1문제를 야기할수있어서, Fetch는 지연로딩으로 설정한다.

PostDto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Getter
@Setter
@ToString
@NoArgsConstructor
public class PostDto {
private Long id;
private String postName;
private String content;
private User user;
private LocalDateTime createdDate;
private LocalDateTime modifiedDate;

public Post toEntity(){
Post post = Post.builder()
.id(id)
.postName(postName)
.content(content)
.user(user)
.build();
return post;
}

@Builder
public PostDto(Long id, String postName, String content, LocalDateTime createdDate, LocalDateTime modifiedDate,User user){
this.id = id;
this.postName = postName;
this.content = content;
this.createdDate = createdDate;
this.modifiedDate = modifiedDate;
this.user = user;
}

UserRepository

1
2
3
public interface UserRepository extends JpaRepository <User, Long > {
Optional<User> findByEmail(String email);
}
  • 현 프로젝트의 unique=true여서 pk값을 대신하는 email 값으로 검증

PostService

1
2
3
4
5
6
7
8
9
10
public Long savePost(String email, PostDto postDto){
User user = userRepository.findByEmail(email).get();
postDto.setUser(user);

//postDto에 User까지 반영후, 그다음에 Entity로 전환 후 저장
Post post = postDto.toEntity();
postRepository.save(post);

return post.getId();
}
  • Post는 User정보가 저장시 반드시 필요함. 서비스에서 savePost() 호출되면, User리포에서 user를 가져와 postDto에 해당 user 반영, 그후 엔티티로 변환후 post리포로 저장.!

PostController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@GetMapping("/{id}")
public String getpost(@PathVariable Long id, Model model, HttpSession session){
UserSessionDto user = (UserSessionDto) session.getAttribute("user");
PostResponseDto postResponseDto = postService.getPost2(id); //추후 나오게될 commentlist를 응답해야함에 따라 새로 getPost()함수만듬

/*유저관련*/
if (user != null){
model.addAttribute("user", user.getEmail()); //login시에 session값에 넣어놓은 user 사용하여, 현사용자 email추출

//게시판 작성자 본인인지 확인
if (postResponseDto.getUser().getEmail().equals(user.getEmail())) {
model.addAttribute("writer", true);
}
}
model.addAttribute("posting", postResponseDto);

return "basic/item";
}

UserService

  • 로그인 성공시 반드시 세션에 필요한 user 정보를 저장할 필요가 있음
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Spring Security 필수 메소드 구현
* 기본적인 반환 타입은 UserDetails, UserDetails를 상속받은 User로 반환 타입 지정 (자동으로 다운 캐스팅됨)
* 특히 session에 user객체를 넘김으로써, 시큐리티 세션에 유저 정보 저장 가능.. 추후 user 정보필요시 사용
* @param email 이메일
* @return UserDetails
* @throws UsernameNotFoundException 유저가 없을 때 예외 발생
*/
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { // 시큐리티에서 지정한 서비스이기 때문에 이 메소드를 필수로 구현
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("해당사용자가 존재하지 않습니다.:" +email));

// 시큐리티 세션에 유저 정보 저장
session.setAttribute("user",new UserSessionDto(user));

return user;

}

UserSessionDto

1
2
3
4
5
6
7
8
9
10
11
12
13
@Getter
public class UserSessionDto implements Serializable {
private String email;
private String nickname;
private String role;

/* Entity -> Dto */
public UserSessionDto(User user) {
this.email = user.getEmail();
this.nickname = user.getNickname();
this.role = user.getRole();
}
}

template

item

  • th:if를 활용함
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div class="row">
<div class="col" th:if="${writer}">
<button class="w-100 btn btn-primary btn-lg"
onclick="location.href='editForm.html'"
th:onclick="|location.href='@{/basic/post/view/{itemId}/edit(itemId=${posting.id})}'|"
type="button">상품 수정
</button>
</div>
<div class="col" th:if="${writer}">
<button class="w-100 btn btn-primary btn-lg"
onclick="location.href='editForm.html'"
th:onclick="|location.href='@{/basic/post/{itemId}/delete(itemId=${posting.id})}'|"
type="button">상품 삭제
</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='items.html'"
th:onclick="|location.href='@{/basic/post/view}'|"
type="button">목록으로
</button>
</div>
CATALOG
  1. 1. Spring 게시판 프로젝트3편 (게시판 작성자만 수정,삭제)
  2. 2. Spring 게시판 프로젝트
  3. 3. 실수한것
    1. 3.1. 왜 PostResponseDto 를 따로 두는지?
    2. 3.2. 아주 좋은 참고자료 JPA 더티체킹은 save()를 대체하지않는다
      1. 3.2.1. 질문
      2. 3.2.2. 답변
  4. 4. 개요
  5. 5. 구현
    1. 5.1. Post
    2. 5.2. PostDto
    3. 5.3. UserRepository
    4. 5.4. PostService
    5. 5.5. PostController
    6. 5.6. UserService
      1. 5.6.1. UserSessionDto
    7. 5.7. template
      1. 5.7.1. item