Skip to content
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
267d34c
first commit
DongCaprio Mar 10, 2025
9aded40
chore : 공백 제거
DongCaprio Mar 10, 2025
60b3ea6
refactor : context-path > yml로 분리("/api")
DongCaprio Mar 11, 2025
0068a07
rebase test
DongCaprio Mar 12, 2025
c2f58aa
rebase test2
DongCaprio Mar 12, 2025
b049194
build : build.gradle에 security 추가
DongCaprio Mar 13, 2025
bf4a443
refactor : 파일 및 폴더 위치 변경
DongCaprio Mar 13, 2025
62a50aa
refactor : 파일 및 폴더 위치 변경
DongCaprio Mar 13, 2025
202c1c5
feat : MemberEntity 추가
DongCaprio Mar 13, 2025
4171948
feat : 회원가입 dto 추가
DongCaprio Mar 13, 2025
b7a6fa4
feat : 회원가입 기능 추가
DongCaprio Mar 13, 2025
7bf9a3c
refactor : 패키지 위치 변경
DongCaprio Mar 14, 2025
3f4a4e8
build : 시큐리티 제거
DongCaprio Mar 14, 2025
cd16eac
feat : 중복 정보로 회원가입 시 예외처리 기능 개발
DongCaprio Mar 14, 2025
6a2ed2e
refactor : 함수분리, 상수화
DongCaprio Mar 15, 2025
9f144e9
feat : 로그인 기능 개발
DongCaprio Mar 16, 2025
e37677d
chore : 서버 안켜지는 오류 수정
DongCaprio Mar 17, 2025
b4b1994
refactor : 로그인, 게시글작성 반환 시 id도 반환하도록 변경
DongCaprio Mar 17, 2025
f7aadcb
feat : 로그인 시 cookie에 jwt 설정
DongCaprio Mar 17, 2025
220f688
test : 회원가입-로그인-jwt검증 테스트 작성
DongCaprio Mar 17, 2025
ba4e971
feat : 필터로 jwt 관련 정보를 필터링 후 로그인한 유저의 email을 RequestAttributes 에 저장
DongCaprio Mar 17, 2025
9073e15
build : data.sql 제거
DongCaprio Mar 18, 2025
5dba48e
refactor : 게시글 작성자 추가
DongCaprio Mar 18, 2025
48f1ed3
feat : 쿠키에 있는 jwt 활용하여 로그인한 사용자인지 검증하는 기능 추가
DongCaprio Mar 18, 2025
31c4f55
feat : 글 작성 시 작성자에 memberId 들어가도록 변경
DongCaprio Mar 18, 2025
a9ffff9
chore : 공백제거
DongCaprio Mar 18, 2025
4134905
feat : OncePerRequestFilter 상속받은 JwtAuthFilter의
DongCaprio Mar 18, 2025
dd2f2ac
fix : OncePerRequestFilter의 doFilterInternal
DongCaprio Mar 19, 2025
789346c
refactor : 변경사항 롤백
DongCaprio Mar 19, 2025
9e927f6
build : 초기 데이터 세팅
DongCaprio Mar 19, 2025
e92f3d4
feat : 게시글 조회 제외한 기능에 jwt 검증 추가
DongCaprio Mar 19, 2025
9f1d15d
test - BlogService Test 작성
DongCaprio Mar 20, 2025
3220a02
test - MemberServiceImpl Test 작성
DongCaprio Mar 20, 2025
716c695
chore : Exception 패키지 위치 수정
DongCaprio Mar 20, 2025
b4750fd
feat : Member 예외처리 CustomException 추가
DongCaprio Mar 20, 2025
2be08da
fix : Artcile 추가/변경 Valid 추가
DongCaprio Mar 20, 2025
9fe5274
test : MemberController 테스트 작성
DongCaprio Mar 20, 2025
6b73e9e
test : BlogApiController 테스트 작성
DongCaprio Mar 20, 2025
54f7ecd
test - BoardApiController Test 작성
DongCaprio Mar 21, 2025
3222659
test - custom exception 변경
DongCaprio Mar 21, 2025
8903b10
chore - 프로젝트 시작 시 들어가는 sql 주석 처리
DongCaprio Mar 21, 2025
033ff4f
refactor : request,response dto 패키지 구분
DongCaprio Mar 25, 2025
6ee53df
fix : AuthUtil.getMemberEmail 메서드에서
DongCaprio Mar 25, 2025
eb5a18f
refactor : 상수 관리 클래스 생성, RequestAttributes null 체크 로직 추가
DongCaprio Mar 25, 2025
e53038a
refactor : JwtAuthFilter 역할 분리
DongCaprio Mar 25, 2025
ade2ea1
refactor : Article > ArticleEntity로 이름 변경
DongCaprio Mar 26, 2025
1863ac5
refactor : 작성자 비교 부분 Article로 로직 이동
DongCaprio Mar 26, 2025
e020c30
refactor : GlobalExceptionHandler 의 예외처리 중복 부분을 제거하기 위해 ErrorType 인터페…
DongCaprio Mar 26, 2025
081b988
refactor : 예외처리 부분 서비스 > 도메인으로 위치 변경
DongCaprio Mar 27, 2025
d340ec5
refactor : 사용하지않는 memberId 제거
DongCaprio Mar 27, 2025
61851c0
test : 실패 test 수정
DongCaprio Mar 27, 2025
60ba393
refactor
DongCaprio Mar 27, 2025
b0143ca
refactor : Entity 기본 생성자 접근제어자 protected로 변경
DongCaprio Mar 27, 2025
7e7d75d
refactor : id 포함 생성자 사용 안하고 test 할 수 있도록 수정
DongCaprio Mar 28, 2025
62ccae5
refactor : id 포함 생성자 사용 안하고 test 할 수 있도록 수정
DongCaprio Mar 28, 2025
d86f2bc
Merge branch 'DongCaprio' of https://github.com/DongCaprio/spring-boa…
DongCaprio Mar 29, 2025
be43658
test : @Nested 사용해서 공통 관심사 묶어서 테스트
DongCaprio Mar 29, 2025
777513c
refactor : 공통 jwt 검증 부분 변경
DongCaprio Apr 1, 2025
968c682
refactor : 필요 없는 필드 제거
DongCaprio Apr 5, 2025
56ac26a
refactor : 필요 없는 클래스 제거
DongCaprio Apr 7, 2025
6e2f6b8
refactor
DongCaprio Apr 9, 2025
440b7fe
refactor, test
DongCaprio Apr 9, 2025
6fdc792
refactor : @AuthenticatedMember 사용법 변경
DongCaprio Apr 9, 2025
27b564b
test : 테스트 변경
DongCaprio Apr 9, 2025
2b4281d
chore : .단위로 메서드 줄바꿈
DongCaprio Apr 9, 2025
70341ff
refactor : PassWord 검증 member객체에서 진행
DongCaprio Apr 10, 2025
5fe8be7
refactor : jwtUtil에서 ACCESS_TOKEN_EXPIRATION(토큰유효시간) 설정하도록 변경
DongCaprio Apr 10, 2025
affab50
refactor : @NotBlank 에러 메세지 추가
DongCaprio Apr 11, 2025
11f7510
refactor : application-test.yml 추가
DongCaprio Apr 11, 2025
51b24de
refactor : application-test.yml 추가
DongCaprio Apr 11, 2025
148058a
Merge branch 'DongCaprio' into DongCaprio
DongCaprio Apr 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}

tasks.named('test') {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/board/BoardApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

@SpringBootApplication
@ConfigurationPropertiesScan
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 요건 왜 다신걸까요?!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

며칠간 서버가 켜질때 오류가 발생하고 서버 구동이 안되었는데
원인을 알고 보니

@Getter
@Component
@ConfigurationProperties("jwt")
@AllArgsConstructor
@NoArgsConstructor
@Setter
public class JwtProperties {
     private String issuer;

이 코드가
@ConfigurationProperties("jwt") 어노테이션을 통해
yml 값을 읽어와서 해당 클래스의 값을 자동으로 넣어주는 부분인데
yml을 값을 넣어주기 위해서는
@ConfigurationPropertiesScan 어노테이션을 달아줘야된다고 봐서 추가했습니다!
(귀신같이 이걸 붙여주니 서버 구동이 잘 되었습니다)

지금 다시 검색해보니
@Component 어노테이션이 있으면
@ConfigurationPropertiesScan 어노테이션이 굳이 필요가 없다고는 하지만..!
그래도 붙여놓는것이 앞으로 @ConfigurationProperties 를 추가할 경우가 있으면 편할 것 같습니다!

public class BoardApplication {

public static void main(String[] args) {
Expand Down
72 changes: 72 additions & 0 deletions src/main/java/com/board/board/controller/BlogApiController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.board.board.controller;

import com.board.board.domain.Article;
import com.board.board.dto.ArticleCreateRequest;
import com.board.board.dto.ArticleResponse;
import com.board.board.dto.ArticleUpdateRequest;
import com.board.board.service.BlogService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RequiredArgsConstructor
@RestController
@RequestMapping("/articles")
public class BlogApiController {

private final BlogService blogService;

@PostMapping("")
public ResponseEntity<ArticleResponse> addArticle(@Valid @RequestBody ArticleCreateRequest request) {
Article savedArticle = blogService.save(request);

return ResponseEntity.status(HttpStatus.CREATED)
.body(new ArticleResponse(savedArticle));
}

@GetMapping("")
public ResponseEntity<List<ArticleResponse>> findAllArticles(@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

Pageable pageable = PageRequest.of(page, size);

List<ArticleResponse> articles = blogService.findAll(pageable)
.stream()
.map(ArticleResponse::new)
.toList();

return ResponseEntity.ok()
.body(articles);
}

@GetMapping("/{id}")
public ResponseEntity<ArticleResponse> findArticle(@PathVariable long id) {
Article article = blogService.findById(id);

return ResponseEntity.ok()
.body(new ArticleResponse(article));
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteArticle(@PathVariable long id) {
blogService.delete(id);

return ResponseEntity.noContent()
.build();
}

@PutMapping("/{id}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Http Method에서 PUT, PATCH의 차이점은 무엇일까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

데이터를 수정함에 있어
전체 수정 : put
부분 수정 : patch 를 의미합니다.

현 메서드의 경우 제목과 내용을 모두 변경하는것이
가능하므로
PUT이 적절한 것 같습니다!

public ResponseEntity<ArticleResponse> updateArticle(@PathVariable long id,
@Valid @RequestBody ArticleUpdateRequest request) {
Article updateArticle = blogService.update(id, request);

return ResponseEntity.ok()
.body(new ArticleResponse(updateArticle));
}
}
54 changes: 54 additions & 0 deletions src/main/java/com/board/board/domain/Article.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.board.board.domain;

import com.board.member.entity.MemberEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@NoArgsConstructor
@Getter
public class Article {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 사용하지 않는 클래스인가요?! 제거해주세요~

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제거 완료했습니다!


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

@Column(name = "title", nullable = false)
private String title;

@Column(name = "content", nullable = false)
private String content;

@ManyToOne(fetch = FetchType.LAZY)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lazy와 Eager의 차이는 무엇일까요? 단순 지연, 즉시로딩이 아닌 어떤 원리로 JPA에서 조회가 되는 걸까요?

학습하시면 관련 키워드로 프록시~~ 를 보실텐데, 그러면 JPA에서 관련 문제로는 어떤 게 생길 수 있을까요?!

Copy link
Author

@DongCaprio DongCaprio Apr 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

연관된 엔티티 가져오는 방법 비교

LAZY : 연관된 엔티티를 프록시 객체로 가지고있음(프록시객체가 실제 엔티티를 찾아올 수는 있어야하므로 실제 엔티티의 식별자를 가지고있음)
그리고 연관 엔티티의 실제 데이터를 조회하려고 할때 DB에서 로드된다.

EAGER : 연관 데이터들까지 모두 즉시 로딩

ex) 작가를 불러오는 쿼리 (작가 테이블과 책 테이블은 연관되어있는 경우)

LAZY : 작가만 불러옴(책 데이터들은 프록시 객체로 가지고있음)

  • 불필요한 데이터 조회 X
  • 연관 데이터를 가져오려고 할 떄 여러번의 쿼리가 발생함
    EAGER : 작가와 연관된 책까지 모두 들고옴
  • 편리하지만 속도 느림

속도 떄문에 실무에서는 대부분 LAZY 전략 채택

  • LAZY 전략은 N+1 문제 발생 가능

N + 1문제 : 쿼리가 내가 원하는것보다 N번 더 발생하는것

ex) 작가 불러오는 쿼리 1번 + 작가에 딸린 책 불러오는 쿼리(작가 갯수만큼) N번 발생

N+1 해결 방법 : JOIN FETCH

JOIN FETCH란 JPA에서 사용하는 작성방법입니다.
실제로는 연관관계 테이블과의 JOIN 쿼리가 발생합니다.
결론 : JOIN FETCH 사용 시 N+1문제 발생하지 않고 데이터를 가져옵니다

@JoinColumn(name = "member_id")
private MemberEntity member;

@Builder
public Article(String title, String content, MemberEntity member) {
this.title = title;
this.content = content;
this.member = member;
}

public Article(Long id, String title, String content, MemberEntity member) {
this.id = id;
this.title = title;
this.content = content;
this.member = member;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 생성자는 언제 사용하나요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사실 원 로직에서는 필요가 없었는데
단위테스트 시 ex)

Article article = new Article(1L, "title", "content", new MemberEntity());
        when(blogRepository.findById(1L)).thenReturn(Optional.of(article));

사용하려고 추가했었습니다,,

테스트 때문에 생성자를 추가하는건 좀 아닌 방법일까요?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트를 위한 프로덕션 코드 추가는 좋지 않다고 생각합니다! 주객전도가 되는 느낌이랄까요?
다른 방법으로 테스트 할 순 없을까요?!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when(blogRepository.findById(anyLong())).thenReturn(Optional.of(article));
blogService.delete(1L);

이런식으로 anyLong()을 이용하면 id 없이도 테스트 할 수 있습니다!
위처럼 테스트를 변경하고
기존 필요없었던 id를 포함한 생성자는 모두 삭제했습니다!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mockito의 기능을 잘 활용하시군요! 좋습니다 ㅎㅎ


public void update(String title, String content) {
this.title = title;
this.content = content;
}
}
31 changes: 31 additions & 0 deletions src/main/java/com/board/board/dto/ArticleCreateRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.board.board.dto;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dto 패키지를 따로 분리해도 좋지만, 학습 단계에선 의존 관계 파악을 위해서 request, response dto의 패키지를 각각 분리해보는 건 어떨까요?

그럼 현재같은 구조에서 RequestDto의 패키지는 어떤 레이어 아래 있어야하는지, Response는 어디에 있는지 고민해볼 수 있을 것 같습니다~

이것도 뭐 사실 굳이지만, 레이어드 아키텍처에서 흐름을 생각해보면서 학습하시면 나중에 의존성을 파악하면서 개발할 수 있는 습관이 들여져 좋은 것 같습니다!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

분리하는것이 보기에 더 좋을 것 같습니다!!
dto 패키지 안에 response, request 패키지를 추가로 만들어서 dto를 구분해서 넣어두었습니다!
말씀해주신 부분이 이게 맞는지 모르겠네요ㅎㅎ


import jakarta.validation.constraints.NotBlank;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@NoArgsConstructor
@Setter
public class ArticleCreateRequest {

@NotBlank
private String title;
@NotBlank
private String content;
private Long memberId;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

게시글 생성할 때 memberId가 넘어오는 걸로 보이는데, 프론트엔드 측에서 로그인 한 유저의 id 값을 어떻게 알 수 있을까요?

프론트 측에서 요청을 보낼 땐 토큰 값 밖에 모를 것 같아요!
그렇다면, 토큰 값으로 id를 추출해서 controller 메서드에 바인딩 할 순 없을까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞습니다! 요청 보낼때 memberId는 알수가 없고 현재 dto에 필요없는 값이기 때문에 삭제했습니다!
save 시 토큰 값으로 member를 추출하는 기능은
blogService에서

public ArticleEntity save(ArticleCreateRequest request) {
        String email = authUtil.getMemberEmail();
        MemberEntity member = memberService.findByEmail(email); // 윗줄과 현재줄에서 member추출

        return blogRepository.save(ArticleEntity.builder()
                .title(request.getTitle())
                .content(request.getContent())
                .member(member)
                .build());
    }

진행하고 있습니다!
이렇게 해도 괜찮을까요?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅎㅎ 넵 사실 저렇게해도 상관없지만, 로그인 정보가 필요한 모든 곳에 AuthUtil을 선언해줘야할 것 같아요
HandlerMethodArgument를 이용해서 어노테이션을 만들고, 요청이 올 때 Controller에 memberId를 바인딩 해주는 방법은 어떨까요?(다른 방법이 있다면 다르게 하셔도 되고요!)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기존 service에서 매번 반복된 코드로 jwt 인증을 처리하던 부분을 말씀해주신 HandlerMethodArgument 과 커스텀 어노테이션을 활용해서
jwt가 필요한 부분 컨트롤러 파라미터(어노테이션)으로 처리하도록 변경했습니다!


public ArticleCreateRequest(String title, String content) {
this.title = title;
this.content = content;
}

@Builder
public ArticleCreateRequest(String title, String content, Long memberId) {
this.title = title;
this.content = content;
this.memberId = memberId;
}
}
22 changes: 22 additions & 0 deletions src/main/java/com/board/board/dto/ArticleResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.board.board.dto;

import com.board.board.domain.Article;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Getter
public class ArticleResponse {

private final Long id;
private final String title;
private final String content;
private final Long memberId;

public ArticleResponse(Article article) {
this.id = article.getId();
this.title = article.getTitle();
this.content = article.getContent();
this.memberId = article.getMember().getId();
}
}
15 changes: 15 additions & 0 deletions src/main/java/com/board/board/dto/ArticleUpdateRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.board.board.dto;

import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class ArticleUpdateRequest {

@NotBlank
private final String title;
@NotBlank
private final String content;
}
7 changes: 7 additions & 0 deletions src/main/java/com/board/board/repository/BlogRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.board.board.repository;

import com.board.board.domain.Article;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BlogRepository extends JpaRepository<Article, Long> {
}
75 changes: 75 additions & 0 deletions src/main/java/com/board/board/service/BlogService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.board.board.service;

import com.board.board.domain.Article;
import com.board.board.dto.ArticleCreateRequest;
import com.board.board.dto.ArticleUpdateRequest;
import com.board.board.repository.BlogRepository;
import com.board.config.auth.AuthUtil;
import com.board.exception.custom.DifferentOwnerException;
import com.board.exception.custom.MyEntityNotFoundException;
import com.board.member.entity.MemberEntity;
import com.board.member.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Objects;

@RequiredArgsConstructor
@Service
@Transactional
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 사소한 팁인데 여기에 readonly=true 속성을 붙이고 업서트 메서드에 @transactional 어노테이션을 선언하는 게 좋아보입니다 ㅎㅎ JPA를 사용하다보니, 실수로 데이터를 수정하는 것이 더 치명적이니깐요!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

팁 감사합니다!
클래스 상위 어노테이션에 readonly를 추가하고
개별 메서드에서 수정/ 삭제 등이 필요한 경우 @transactional 을 추가했습니다!

public class BlogService {

private final BlogRepository blogRepository;
private final MemberService memberService;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MemberService와 MemberRepository를 가져다 쓰는 것과 어떤 차이가 있을까요? (사실 정답은 없지만) 동진님이 생각하시는 차이가 궁금합니다 ㅎㅎ

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MemberService 를 사용하는것이 좋다고 생각합니다.

Service : 기능 구현
Repository : DB접근 이라는 역할이 있으니
그에 맞게 Service에 기능을 활용하는것이 적절한 것 같습니다.
단순히 db에 접근하는것도 service에서 구현하여 service의 메서드를 이용하는것이 맞는 선택인 것 같습니다.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

woowacourse/retrospective#15

여기 보시면 많은 분들이 토론하신 게 있는데 보시면 좋을 것 같아요~
의존성을 끊으면 더 좋긴합니다! (비즈니스적인 의존 관계가 없다고 쳤을 때) id값을 토큰으로 빼와서 가져오면서 이를 끊을 수도 있고 여러 가지 방법이 있겠네요. 다만 ManyToOne과 같이 연관관계 매핑을 했다면 힘들 수도 있겠네요

Copy link
Author

@DongCaprio DongCaprio Apr 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

의존성을 끊는다는 것이
BlogService 에서 MemberService 관련된 값이 없어도 작동하도록
즉 현재 save를 예를 들면 BlogService 자체적으로 저장이 가능하도록 하면 좋다는 말씀이시죠?

이것 참 어렵네요
현재 말씀해주신것처럼 @manytoone 연관관계 매핑이 있기 때문에
BlogService 를 통해 게시글에 작성자를 넣는것이 알맞다고 생각합니다!

@Transactional
    public ArticleEntity save(ArticleCreateRequest request, Long memberId) {
        return blogRepository.save(ArticleEntity.builder()
                .title(request.getTitle())
                .content(request.getContent())
                .member(memberService.findById(memberId))
                .build());
    }

저번에 실무에서는 @manytoone 을 별로 사용하지 않고
id만 저장하는 간접참조 방식을 많이 사용한다고 말씀해주셨었는데
이런 의존성을 줄이기 위해서 사용이 되는것 같다는 합리적 추측이...
하지만 현재 구조에서는 지금 방식도 괜찮은 방식이라고 생각합니다...!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅎㅎ 네 모든 상황이 다 다르니깐요!
만약 저기서 간접참조를 썼다면 MemberService를 import할 필요는 없었을 것 같아서요
HandlerMethodArgumentResolver에서 토큰을 통해 로그인한 유저의 MemberId를 받아서 생성자에 넣어서 의존성을 분리할 수도 있습니다!

https://www.youtube.com/watch?v=dJ5C4qRqAgA

이 내용 관련해서 진짜 좋은 영상인데 좀 긴데 꼭 보셨으면 좋겠습니다! 어떤 내용인지 확 와닿으실거에요!
제가 말씀 드리는 것도 전부 정답은 아니고 하나의 방법이기 때문에 참고만 해주세요!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

영상도 찾아주시고 정말 정말 감사합니다!!
해당 영상을 봤는데 아직은...ㅎㅎ 사실 어렵네요

  1. 핵심은 시스템이 커질수록 직접참조보다 간접참조 방식을 권장
  2. 양방향 의존성을 줄이자
    그 방법으로
    의존성의 연결고리? 같은 중간 객체를 만드는것과
    도메인 이벤트 등을 설명해주시는것 같은데
    영상을 다시 돌려보고 모르는 부분(도메인 이벤트 등)을 더 검색해나가면서 이해해보도록 하겠습니다!!!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅎㅎ 넵 영상에서 결국 참조관계는 양방향보다 단방향인 경우 유지보수도 그렇고 여러 장점이 있다는 것이고 그러기에 간접참조 얘기가 나온 거에요! 아마 지금은 힘들 수 있지만, 천천히 보시고 직접 느껴보면서 관련 자료 찾아보시다보면 금방 이해하실 것 같습니다!

private final AuthUtil authUtil;

public Article save(ArticleCreateRequest request) {
String email = authUtil.getMemberEmail();
MemberEntity member = memberService.findByEmail(email);

return blogRepository.save(Article.builder()
.title(request.getTitle())
.content(request.getContent())
.member(member)
.build());
}

@Transactional(readOnly = true)
public Page<Article> findAll(Pageable pageable) {
return blogRepository.findAll(pageable);
}

@Transactional(readOnly = true)
public Article findById(long id) {
return findArticle(id);
}

public void delete(long id) {
compareAuthors(id);
blogRepository.deleteById(id);
}

public Article update(long id, ArticleUpdateRequest request) {
Article article = compareAuthors(id);
article.update(request.getTitle(), request.getContent());
return article;
}

private Article compareAuthors(long articleId) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분도 작성자를 검증한다면 Article의 비즈니스 로직이라고 생각해서 Article 안에 캡슐화해야 한다고 생각하는데, 어떻게 생각하시는지 궁금합니다~

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

compareAuthors 메서드에 모든 부분보다
db를 통하는부분, authUtil를 사용하는 부분은 기존처럼 남겨두고
Article에서는 작성자가 같은지 판단하는 메서드를 가지는것이 좋다고 생각합니다!
(에러 던지는 부분은 service에 두어야할지, Article에 두어야 할지 잘 모르겠네요,,)
말씀드린대로 수정해보았습니다

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅎㅎ 맞습니다 저도 공감합니다! 에러 던지는 부분은 id가 같은지 확인하는 게 동진님이 정한 비즈니스 로직이라면 도메인에서 진행해도 괜찮을 것 같습니다!

Copy link
Author

@DongCaprio DongCaprio Mar 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

도메인에서 유효한 작성자가 아닐시 에러 발생시키도록 수정 완료했습니다!

public void validateOwner(MemberEntity member) {
        if (!this.member.getId().equals(member.getId())) {
            throw DifferentOwnerException.from(this.member.getEmail());
        }
    }

감사합니다!

String email = authUtil.getMemberEmail();
MemberEntity member = memberService.findByEmail(email);
Article article = findArticle(articleId);
if (!Objects.equals(member.getId(), article.getMember().getId())) {
throw DifferentOwnerException.from(article.getMember().getEmail());
}
return article;
}

private Article findArticle(long id) {
return blogRepository.findById(id)
.orElseThrow(() -> MyEntityNotFoundException.from(id));
}
}
58 changes: 58 additions & 0 deletions src/main/java/com/board/config/DataInitializer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.board.config;

import com.board.board.repository.BlogRepository;
import com.board.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;

@Configuration
@RequiredArgsConstructor
public class DataInitializer {

private final MemberRepository memberRepository;
private final BlogRepository blogRepository;
/* 테스트에 방해되서 주석처리
@Bean
public CommandLineRunner initData() {
return args -> {
// 기본 회원 데이터 생성
MemberEntity member1 = MemberEntity.builder()
.email("aa@aa.com")
.password("1234")
.nickName("aa")
.build();

MemberEntity member2 = MemberEntity.builder()
.email("aa@aa.com2")
.password("1234")
.nickName("aa2")
.build();

memberRepository.save(member1);
memberRepository.save(member2);

// 기본 게시글 데이터 생성
Article article1 = Article.builder()
.title("First Article")
.content("Content of the first article")
.member(member1)
.build();

Article article2 = Article.builder()
.title("Second Article")
.content("Content of the second article")
.member(member1)
.build();

Article article3 = Article.builder()
.title("Third Article")
.content("Content of the third article")
.member(member2)
.build();

blogRepository.save(article1);
blogRepository.save(article2);
blogRepository.save(article3);
};
}*/
}
19 changes: 19 additions & 0 deletions src/main/java/com/board/config/auth/AuthUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.board.config.auth;

import static com.board.config.jwt.JwtAuthFilter.AUTHENTICATED_USER;

import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

@Component
public class AuthUtil {
public String getMemberEmail() {
return (String) RequestContextHolder.getRequestAttributes()
.getAttribute(AUTHENTICATED_USER, RequestAttributes.SCOPE_REQUEST);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getAttribute에서 null 값이면 500에러가 발생하지 않을까요? 디버깅하기 힘들 것 같습니다!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

null일때는 server custom Exception을 발생시키도록 수정했습니다!!

}

public boolean isAuthenticated() {
return getMemberEmail() != null;
}
}
Loading