본문 바로가기

개인프로젝트/RandB

[Red&Blue]토론게시판 CRUD만들기 등록에 대하여

문제 상황

 

 

 
 

토론 게시판에 등록,조회,수정,삭제 기능을 만들어야하는데  모든 코드들을 보면 setter를 사용하여 데이터를 생성하지 않고 생성자를 만들어서 get하여 캡슐화를 하는 것을 알 수 있다. 

 

또한 빌더패턴사용에 대해 알아보고 JPA로 작업하면서 사용된 어노테이션에 대해서 알아보자. 

 

 


요구 사항

 

 



1. JPA의 작성기능 흐름에 대해 알아보자.

2. 사용된 어노테이션에 대해 이해해보자.

3. 빌터 패턴 이전의 순수 자바문법을 이해해보자.

 

 


필요 개념

 

1. JPA 영속성 컨텍스트에 엔티티 저장

영속성 컨텍스트엔티티를 영속 상태로 관리하는 JPA의 메커니즘이다. 쉽게 말해, 애플리케이션과 데이터베이스 사이의 중간 캐시 역할을 하며, 엔티티의 상태 변화를 추적하고, 데이터베이스와의 동기화를 책임진다.

save() 호출 시 동작 과정

  1. 비영속 상태: 엔티티 객체는 new 키워드로 생성되며 이 상태에서는 영속성 컨텍스트와 전혀 관련이 없다. 이 상태에서는 데이터베이스에 저장되지 않는다.

  2. save() 호출:
    • save() 메서드를 호출하면, 해당 엔티티는 영속성 컨텍스트에 저장된다.
    • 영속성 컨텍스트는 해당 엔티티의 상태를 추적하고, 데이터베이스와 동기화할 준비를 한다.
  3. 영속 상태(Persistent State):
    • save() 메서드가 호출된 후, 엔티티는 영속성 컨텍스트에 의해 관리되는 영속 상태가 된다.
    • 이때 JPA는 기본적으로 해당 엔티티의 변화(필드 값 변경 등)를 감지하며, 트랜잭션이 커밋될 때 자동으로 데이터베이스에 반영한다.
    • 예를 들어, postRepository.save(post)를 호출하면 Post 객체는 영속성 컨텍스트에서 관리되고, 트랜잭션이 끝날 때 INSERT 또는 UPDATE SQL 쿼리가 데이터베이스에 실행된다.
  4. 1차 캐시: 영속성 컨텍스트는 엔티티를 메모리 상에서 1차 캐시에 저장한다. 따라서 동일한 트랜잭션 내에서 같은 엔티티를 여러 번 조회할 경우, 데이터베이스에 직접 접근하지 않고 캐시에서 데이터를 반환한다. 이는 성능 최적화에 도움을 준다.
  5. 쓰기 지연 (Write-behind) 및 트랜잭션:
    • 영속성 컨텍스트는 데이터베이스에 바로 쿼리를 실행하지 않고, 트랜잭션이 커밋될 때까지 변경된 엔티티 정보를 일시적으로 저장해 둔다.
    • 트랜잭션이 커밋되면, JPA는 영속성 컨텍스트에 저장된 엔티티 상태를 기준으로 INSERT, UPDATE, DELETE 등의 쿼리를 실행하여 데이터베이스에 반영한다. 이 과정을 쓰기 지연 (Write-behind)라고 한다.

빌터패턴의 사용(롬복)

캡슐화와 setter 문제점

  • 캡슐화는 객체의 내부 상태를 외부에서 함부로 접근하지 못하게 보호하는 개념이다. 하지만 setter 메서드를 제공하면 외부에서 자유롭게 객체의 상태를 변경할 수 있어, 캡슐화가 제대로 유지되지 않을 수 있다.
  • setter를 통해 객체의 상태가 변경되면 예상치 못한 방식으로 동작할 가능성이 생기고 유지보수 과정에서 문제가 발생할 수 있다.

생성자를 통한 객체 생성

  • 생성자를 통해 필수적인 값들을 객체 생성 시 한 번에 전달하여 초기화하는 방식은 객체가 완전히 설정된 상태에서만 생성되도록 강제할 수 있다.
  • 이렇게 생성된 객체는 더 이상 상태를 변경하지 않으므로 불변성을 갖게 되며 이를 통해 코드의 안전성과 일관성이 높아진다.

코드 설명

@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Post {
    // @Entity: JPA가 관리하는 클래스임을 선언. 데이터베이스의 테이블과 매핑되어 JPA를 통해서 DB와 상호작용할 수 있게 함.

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "post_id")
    private Long id;
    // @Id: 해당 필드를 테이블의 기본 키(Primary Key)로 설정.
    // @GeneratedValue(strategy = GenerationType.IDENTITY): 기본 키 값을 자동으로 생성. IDENTITY 전략은 데이터베이스에 의존하여 값이 생성되며, 주로 MySQL 같은 DB에서 사용됨.
    // @Column(name = "post_id"): 데이터베이스 테이블에서 이 필드가 "post_id"라는 이름의 컬럼에 매핑됨.

    @Column(name = "post_title")
    private String postTitle;
    // @Column(name = "post_title"): 이 필드를 데이터베이스의 "post_title" 컬럼에 매핑함. (name 속성은 DB 컬럼명을 명시적으로 지정할 때 사용)

    @Column(name = "post_content", length = 1000)
    private String postContent;
    // @Column(name = "post_content", length = 1000): "post_content" 컬럼에 매핑되며, 최대 길이가 1000자로 제한됨.

    private LocalDateTime created_at;
    // 필드명과 동일한 컬럼 이름으로 매핑. JPA에서 기본적으로 필드명을 DB 컬럼명으로 사용함. 이 경우 생성된 시간 정보를 저장함.

    private LocalDateTime updated_at;
    // 업데이트된 시간을 저장할 필드. 이 필드는 엔티티가 업데이트될 때 시간을 저장하는 데 사용됨.
}

어노테이션 설명

@Entity
JPA에서 해당 클래스를 엔티티(데이터베이스의 테이블과 매핑되는 객체)로 선언. 이 어노테이션을 붙이면 JPA가 해당 클래스와 관련된 DB 테이블을 관리. 클래스명이 데이터베이스의 테이블명으로 매핑되며, 따로 지정하지 않으면 클래스명을 그대로 테이블명으로 사용함.

 

@Id

기본 키를 정의하는 어노테이션. JPA에서 엔티티는 반드시 기본 키가 필요하며, 이 어노테이션을 통해 어떤 필드가 기본 키인지를 명시함.

 

@GeneratedValue:

기본 키 생성 전략을 지정하는 어노테이션.

strategy = GenerationType.IDENTITY: 기본 키 값을 데이터베이스가 자동으로 생성하도록 함.

DENTITY 전략은 데이터베이스의 AUTO_INCREMENT 기능을 사용하여 기본 키 값을 자동으로 생성함.

 

@Column

해당 필드를 DB 테이블의 특정 컬럼에 매핑할 때 사용.

name: 데이터베이스에서 사용할 컬럼명을 명시적으로 지정.

length: 문자열의 최대 길이를 설정할 수 있으며, 이 경우에는 post_content의 최대 길이를 1000자로 제한함.

 

@Data

롬복(Lombok) 라이브러리에서 제공하는 어노테이션으로, 엔티티의 Getter, Setter, equals, hashCode, toString 메서드를 자동으로 생성해 줌.

@Builder

빌더 패턴을 적용하는 어노테이션. 객체를 생성할 때 new 키워드 대신 가독성이 좋은 방식으로 생성할 수 있음.

Post.builder().postTitle("title").postContent("content").build()와 같은 방식으로 객체를 만들 수 있음.

 

@AllArgsConstructor

롬복 어노테이션으로 모든 필드를 파라미터로 받는 생성자를 자동으로 생성.

 

@NoArgsConstructor

파라미터가 없는 기본 생성자를 자동으로 생성해 줌. JPA는 엔티티를 관리할 때 기본 생성자가 필요하기 때문에, JPA 엔티티에서는 이 어노테이션을 자주 사용.

 

@RequiredArgsConstructor // final 필드에 대해 자동으로 생성자를 만들어주는 Lombok 어노테이션. 의존성 주입을 위한 생성자를 작성하지 않아도 됨.
@RestController // Spring MVC에서 사용되는 어노테이션으로, 해당 클래스가 REST API의 컨트롤러임을 나타냄. 주로 JSON 또는 XML 형태의 응답을 반환함.
@Slf4j // Lombok에서 제공하는 어노테이션으로, 자동으로 로깅(logging) 기능을 위한 Logger 객체를 생성해줌. 클래스 내에서 log.debug, log.info 등의 메서드를 사용 가능.
public class PostController {

    private final PostService postService; // 의존성 주입을 통해 PostService 객체를 주입받음. @RequiredArgsConstructor가 생성자를 자동으로 생성해줌.

    @PostMapping(value = "api/user/posts") // HTTP POST 요청을 처리하는 메서드임을 나타내며, "/api/user/posts" URL로 요청이 들어오면 해당 메서드가 실행됨.
    public ResponseEntity<?> postAdd(@Valid UserAddRequest userAddPostDto) {
        // @Valid: UserAddRequest DTO를 유효성 검사함. 유효성 검사가 실패하면 자동으로 예외를 발생시킴.
        // UserAddRequest: 게시물 작성 시 필요한 요청 데이터를 담는 DTO 객체. 이 데이터는 Post 엔티티로 변환되어 저장됨.

        postService.save(userAddPostDto); // PostService의 save 메서드를 호출하여 게시물 데이터를 저장함. 여기서 JPA의 save() 메서드가 호출되어 DB에 영속성 컨텍스트에 반영됨.

        return ResponseEntity.ok(new ControllerApiResponse(true, "작성 성공")); 
        // ResponseEntity는 HTTP 응답을 만들어 반환함. 여기서는 상태 코드 200 OK와 함께 작성 성공 메시지를 반환함.
    }
}

어노테이션 설명

@RequiredArgsConstructor

final 키워드가 붙은 필드에 대한 생성자를 자동으로 생성해주는 Lombok 어노테이션.

필드 주입 대신 생성자 주입을 권장하는 이유는, 객체가 생성될 때 의존성이 주입되며, 불변성을 보장할 수 있기 때문.

 

@RestController

Spring의 REST API 컨트롤러를 정의할 때 사용.

이 어노테이션이 붙은 클래스는 JSON이나 XML 형태의 데이터를 직접 반환함. 내부적으로 @Controller와 @ResponseBody가 결합된 형태임.

 

@Slf4j:

Lombok의 로깅 어노테이션으로, 클래스에 Logger 객체를 자동으로 생성해줌.

개발자는 별도의 로거 설정 없이 log.info, log.debug 등의 메서드를 사용해 로깅을 할 수 있음.

 

@PostMapping

HTTP POST 요청을 처리하는 메서드를 정의할 때 사용됨.

여기서는 "/api/user/posts" 경로로 들어오는 POST 요청을 처리함.

 

@Valid

UserAddRequest DTO 객체의 유효성을 검증하는 데 사용됨.

유효성 검사가 실패하면 MethodArgumentNotValidException 예외를 발생시켜 처리함.

 

public interface PostService {

    void save(UserAddRequest userAddRequest);

}

 

@Transactional // 메서드 또는 클래스에 트랜잭션을 적용하는 JPA 관련 어노테이션. 메서드가 실행될 때 트랜잭션이 시작되고, 메서드가 정상적으로 끝나면 트랜잭션이 커밋됨. 예외가 발생하면 자동으로 롤백.
@RequiredArgsConstructor // Lombok 어노테이션으로, final 필드에 대해 생성자를 자동으로 생성해줌. PostRepository를 주입받기 위해 사용.
@Service // Spring의 서비스 계층을 나타내는 어노테이션. 비즈니스 로직을 처리하는 클래스임을 알림.
@Slf4j // Lombok 어노테이션으로, 로깅을 위한 Logger 객체를 자동으로 생성해줌. log.info(), log.error() 등으로 로그를 기록할 수 있음.

public class PostServiceImpl implements PostService { // PostService 인터페이스를 구현한 서비스 클래스.

    private final PostRepository postRepository; // 의존성 주입을 통해 PostRepository 객체를 주입받음. Repository는 데이터베이스에 접근하는 역할을 담당.

    @Override
    public void save(UserAddRequest userAddRequest) { // 게시물 저장 로직을 처리하는 메서드. 

        Post post = Post.builder() // Post 엔티티의 빌더 패턴을 사용하여 객체를 생성함.
                .postTitle(userAddRequest.getPostTitle()) // 요청 객체로부터 제목을 가져와 Post 엔티티에 설정.
                .postContent(userAddRequest.getPostContent()) // 요청 객체로부터 본문 내용을 가져와 Post 엔티티에 설정.
                .created_at(LocalDateTime.now()) // 현재 시간을 생성 일시로 설정.
                .build(); // Post 객체를 완성.

        postRepository.save(post); // JPA의 save() 메서드를 통해 생성된 Post 엔티티를 데이터베이스에 저장. 이때 영속성 컨텍스트에 반영됨.
    }
}

@Transactional

메서드 실행 시 트랜잭션이 시작되고, 메서드가 정상적으로 완료되면 커밋됨. 만약 예외가 발생하면 자동으로 롤백하여 데이터의 무결성을 보장함. 클래스에 붙이면 해당 클래스의 모든 public 메서드에 트랜잭션이 적용됨.

 

@Service

Spring에서 비즈니스 로직을 처리하는 서비스 계층을 나타내는 어노테이션.

@Component의 특수화된 어노테이션으로, Spring의 컴포넌트 스캔에 의해 자동으로 빈으로 등록됨.

 




빌더 패턴의 기능

  1. 객체 생성 시 가독성 향상: 빌더 패턴은 객체를 생성할 때 어떤 필드에 어떤 값이 들어가는지 명확하게 표현한다. 특히, 필드가 많은 경우 필드 순서를 기억할 필요가 없어서 가독성이 높아진다.
  2. 유연한 객체 생성: 모든 필드를 꼭 초기화하지 않아도 되며, 선택적으로 필요한 필드만 초기화할 수 있다.
  3. 불변성 유지: 객체가 생성된 이후에는 상태가 변경되지 않도록 할 수 있다. 불변성은 여러 스레드에서 안전하게 사용할 수 있도록 도와준다.
 

1. 생성자를 통한 객체 생성

생성자를 통해 모든 필드를 설정하는 방식이다. 이는 필드의 순서에 주의해야 하고, 필드가 많을 경우 코드가 지저분해질 수 있다.

객체를 생성하면서 동시에 인자를 넣어서 사용 하는 것을 볼 수 있다.

public void save(UserAddRequest userAddRequest) {

    // 생성자를 통해 Post 객체를 생성
    Post post = new Post(
            userAddRequest.getPostTitle(),
            userAddRequest.getPostContent(),
            LocalDateTime.now(), // created_at      
    );

    // 저장
    postRepository.save(post);
}

2. setter를 통한 객체 생성

생성자 대신 setter 메서드를 사용해 필요한 값만 설정하는 방식이다. 이 방식은 가독성이 조금 더 나아지지만, 객체가 완전히 생성되기 전까지는 불완전한 상태가 될 수 있어, 그다지 선호되는 방식은 아니다.

생성자를 호출한 후 값을 새로 세팅하는것을 볼 수 있다.

public void save(UserAddRequest userAddRequest) {

    Post post = new Post(); // 기본 생성자 호출
    
    post.setPostTitle(userAddRequest.getPostTitle());
    post.setPostContent(userAddRequest.getPostContent());
    post.setCreatedAt(LocalDateTime.now());

    // 저장
    postRepository.save(post);
}

 

 

public interface PostRepository extends JpaRepository<Post,Long>{

}

왜 save() 메서드를 따로 정의하지 않아도 되는가?

JpaRepository 인터페이스는 JPA와 관련된 다양한 기본 메서드들을 제공하며 Spring Data JPA가 자동으로 이를 구현해 준다. 

부모레포지토리에 구현되어 있어 상속 받고 있다.

  • save(): 새로운 엔티티를 저장하거나, 동일한 id를 가진 엔티티가 있으면 이를 수정함.
  • findById(): 특정 ID로 엔티티를 조회함.
  • findAll(): 모든 엔티티를 조회함.
  • deleteById(): 특정 ID를 가진 엔티티를 삭제함.
  • count(): 엔티티의 총 개수를 반환함.

 

'개인프로젝트 > RandB' 카테고리의 다른 글

[Red&Blue] spring에 chatGPT테스트(Spring AI)  (5) 2024.09.25
[Red&Blue] 서비스기획안  (1) 2024.09.24
[Red&Blue] 개발환경설정  (0) 2024.07.04