CRUD 복습해보기

정다운·2025년 7월 10일

멋사

목록 보기
1/1
post-thumbnail

멋쟁이 사자처럼 13기 강의를 진행하며 만든 자료를 블로그에 공유합니다.

CRUD와 예외처리 이론 복습

CRUD

Create데이터 생성POST
Read데이터 조회GET
Update데이터 수정PUT / PATCH
Delete데이터 삭제DELETE

Domain(Entity 클래스)

도메인은 실제 데이터베이스와 매핑되며 시스템의 핵심 데이터를 표현하는 클래스

각 필드는 데이터베이스의 컬럼과 연결되며, 비즈니스 중심의 핵심 데이터를 담고 있다

  • Member(예은님 CRUD 강의자료 참고)
    package com.likelion.likelioncrud.member.domain;
    
    import com.likelion.likelioncrud.member.api.dto.request.MemberUpdateRequestDto;
    import com.likelion.likelioncrud.post.domain.Post;
    import jakarta.persistence.*;
    import lombok.AccessLevel;
    import lombok.Builder;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    
    import java.util.ArrayList;
    import java.util.List;
    
    @Entity
    @Getter
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    public class Member {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "member_id")
        private Long memberId;
    
        private String name;
    
        private int age;
    
        @Enumerated(EnumType.STRING)
        @Column(length = 20)
        private Part part;
    
        @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
        private List<Post> posts = new ArrayList<>();
    
        @Builder
        private Member(String name, int age, Part part) {
            this.name = name;
            this.age = age;
            this.part = part;
        }
    
        public void update(MemberUpdateRequestDto memberUpdateRequestDto) {
            this.name = memberUpdateRequestDto.name();
            this.age = memberUpdateRequestDto.age();
        }
    }

Repository

데이터베이스에 직접 접근하는 계층으로, Spring Data JPA가 제공하는 JpaRepository를 상속받아 기본적인 CRUD 메서드를 사용할 수 있다

도메인 객체를 저장하거나 조회할 때 이 계층을 사용한다

  • MemberRepository (예은님 CRUD 강의자료 참고)
    package com.likelion.likelioncrud.member.domain.repository;
    
    import com.likelion.likelioncrud.member.domain.Member;
    import org.springframework.data.jpa.repository.JpaRepository;
    
    public interface MemberRepository extends JpaRepository<Member, Long> {
    }

DTO

DTO는 클라이언트와 서버 간에 데이터를 전송하기 위한 객체이다

요청(Request)과 응답(Response)에 따라 서로 다른 구조로 설계한다

  • MemberSaveRequestDto (예은님 CRUD 강의자료 참고)
    package com.likelion.likelioncrud.member.api.dto.request;
    
    import com.likelion.likelioncrud.member.domain.Part;
    
    public record MemberSaveRequestDto(
            String name,
            int age,
            Part part
    ) {
    }

Service

비즈니스 로직(실제 처리 내용)을 수행하는 계층이다

데이터 저장, 조회, 연산 등 핵심 처리 담당

  • MemberService (예은님 CRUD 강의자료 참고)
    package com.likelion.likelioncrud.member.application;
    
    import java.util.List;
    
    import com.likelion.likelioncrud.member.api.dto.request.MemberSaveRequestDto;
    import com.likelion.likelioncrud.member.api.dto.request.MemberUpdateRequestDto;
    import com.likelion.likelioncrud.member.api.dto.response.MemberInfoResponseDto;
    import com.likelion.likelioncrud.member.api.dto.response.MemberListResponseDto;
    import com.likelion.likelioncrud.member.domain.Member;
    import com.likelion.likelioncrud.member.domain.repository.MemberRepository;
    import lombok.RequiredArgsConstructor;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    @Service
    @RequiredArgsConstructor
    @Transactional(readOnly = true)
    public class MemberService {
        private final MemberRepository memberRepository;
    
        // 사용자 정보 저장
        @Transactional
        public void memberSave(MemberSaveRequestDto memberSaveRequestDto) {
            Member member = Member.builder()
                    .name(memberSaveRequestDto.name())
                    .age(memberSaveRequestDto.age())
                    .part(memberSaveRequestDto.part())
                    .build();
            memberRepository.save(member);
        }
    
        // 사용자 모두 조회
        public MemberListResponseDto memberFindAll() {
            List<Member> members = memberRepository.findAll();
            List<MemberInfoResponseDto> memberInfoResponseDtoList = members.stream()
                    .map(MemberInfoResponseDto::from)
                    .toList();
            return MemberListResponseDto.from(memberInfoResponseDtoList);
        }
    
        // 단일 사용자 조회
        public MemberInfoResponseDto memberFindOne(Long memberId) {
            Member member = memberRepository
                    .findById(memberId)
                    .orElseThrow(IllegalArgumentException::new);
            return MemberInfoResponseDto.from(member);
        }
    
        // 사용자 정보 수정
        @Transactional
        public void memberUpdate(Long memberId, MemberUpdateRequestDto memberUpdateRequestDto) {
            Member member = memberRepository.findById(memberId).orElseThrow(IllegalArgumentException::new);
            member.update(memberUpdateRequestDto);
        }
    
        // 사용자 정보 삭제
        @Transactional
        public void memberDelete(Long memberId) {
            Member member = memberRepository.findById(memberId).orElseThrow(IllegalArgumentException::new);
            memberRepository.delete(member);
        }
    }

Controller

사용자의 HTTP 요청을 받아 처리하고 응답을 돌려주는 역할

적절한 HTTP 메서드와 함께 @RequestMapping, @PostMapping, @GetMapping 등을 사용하여 API를 정의한다

  • MemberController (예은님 CRUD 강의자료 참고)
    package com.likelion.likelioncrud.member.api;
    
    import com.likelion.likelioncrud.member.api.dto.request.MemberSaveRequestDto;
    import com.likelion.likelioncrud.member.api.dto.request.MemberUpdateRequestDto;
    import com.likelion.likelioncrud.member.api.dto.response.MemberInfoResponseDto;
    import com.likelion.likelioncrud.member.api.dto.response.MemberListResponseDto;
    import com.likelion.likelioncrud.member.application.MemberService;
    import lombok.RequiredArgsConstructor;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.*;
    
    @RestController
    @RequiredArgsConstructor
    @RequestMapping("/member")
    public class MemberController {
    
        private final MemberService memberService;
    
        // 사용자 저장
        @PostMapping("/save")
        public ResponseEntity<String> memberSave(@RequestBody MemberSaveRequestDto memberSaveRequestDto) {
            memberService.memberSave(memberSaveRequestDto);
            return new ResponseEntity<>("사용자 저장!", HttpStatus.CREATED);
        }
    
        // 사용자 전체 조회
        @GetMapping("/all")
        public ResponseEntity<MemberListResponseDto> memberFindAll() {
            MemberListResponseDto memberListResponseDto = memberService.memberFindAll();
            return new ResponseEntity<>(memberListResponseDto, HttpStatus.OK);
        }
    
        // 회원 id를 통해 특정 사용자 조회
        @GetMapping("/{memberId}")
        public ResponseEntity<MemberInfoResponseDto> memberFindOne(@PathVariable("memberId") Long memberId) {
            MemberInfoResponseDto memberInfoResponseDto = memberService.memberFindOne(memberId);
            return new ResponseEntity<>(memberInfoResponseDto, HttpStatus.OK);
        }
    
        // 회원 id를 통한 사용자 수정
        @PatchMapping("/{memberId}")
        public ResponseEntity<String> memberUpdate(@PathVariable("memberId") Long memberId,
                                                   @RequestBody MemberUpdateRequestDto memberUpdateRequestDto) {
            memberService.memberUpdate(memberId, memberUpdateRequestDto);
            return new ResponseEntity<>("사용자 수정", HttpStatus.OK);
        }
    
        // 회원 id를 통한 사용자 삭제
        @DeleteMapping("/{memberId}")
        public ResponseEntity<String> memberDelete(@PathVariable("memberId") Long memberId) {
            memberService.memberDelete(memberId);
            return new ResponseEntity<>("사용자 삭제", HttpStatus.OK);
        }
    }

Rest API 설계 원칙

URI는 정보의 리소스를 표현해야 한다

// REST를 제대로 적용하지 않은 URI 예시
GET /members/delete/1
  • 행위에 대한 표현이 아닌 리소스를 표현하는데 중점을 두어야 한다
  • 리소스명은 동사보다는 명사를 사용한다
// REST 적용 예시 1
GET /members/delete/1 (X)
DELETE /members/1 (O)

// REST 적용 예시 2
GET /members/show/1 (X)
GET /members/1 (O)

// REST 적용 예시 3
GET /members/insert/3 (X)
POST /members/3 (O)
  • 슬래시 구분자(/)는 계층 관계를 나타내는데 사용한다
  • URI 마지막 문자로 슬래시(/)를 표한하지 않는다
  • 하이픈(-)은 URI 가독성을 높이는데 사용할 수 있다
  • 언더바(_)는 URI에 사용하지 않는다
  • URI 경로에는 소문자를 사용한다

EX) REST는 자원 중심 설계 지향 → /getUser는 REST 위반

올바른 방식 : /user/{id} + GET

예외 상황별 HTTP 상태코드

예외 상황HTTP 코드설명
잘못된 입력값400 Bad Request필드 유효성 검증 실패
리소스 없음404 Not Found존재하지 않는 ID
서버 내부 오류500 Internal Server ErrorNullPointer, DB 다운 등
인증 실패401 UnauthorizedJWT 누락, 로그인 필요
권한 없음403 Forbidden접근 권한 없음

예외 처리

ErrorCode, SuccessCode 정의

프로젝트 전반에서 사용될 성공, 실패 응답 코드를 한 곳에 정의

응답 메시지, 상태 코드, 코드명을 함께 정의하여 일관된 응답을 생성

예시:

  • NOT_FOUND_404("해당 리소스를 찾을 수 없습니다.")
  • INTERNAL_SERVER_ERROR_500("서버 내부 오류입니다.")

BusinessException : 커스텀 예외 클래스

비즈니스 로직 중 발생 가능한 예외를 처리하기 위한 커스텀 예외 클래스

각 예외에 적절한 ErrorCode를 전달하여 일관된 응답 제공

  • BusinessException(하영님 예외처리 강의자료 참고)
    package com.likelion.likelioncrud.common.exception;
    
    import com.likelion.likelioncrud.common.error.ErrorCode;
    import lombok.Getter;
    
    @Getter // getter 메소드 자동 생성 lombok 어노테이션
    public class BusinessException extends RuntimeException {
    
        private final ErrorCode errorCode;   // 에러 코드 ex) NOT_FOUND
        private final String customMessage;  // 사용자 정의 예외 메시지(커스텀)
    
    		// 생성자
        public BusinessException(ErrorCode errorCode, String customMessage) {
            super(customMessage);    // RuntimeException의 생성자에 메시지 전달
            this.errorCode = errorCode;
            this.customMessage = customMessage;
        }
    }

ApiResTemplate : 공통 응답 포맷

모든 API 응답을 동일한 포맷으로 구성하기 위한 템플릿

code, message, data 구조

  • ApiResTemplate(하영님 예외처리 강의자료 참고)
    package com.likelion.likelioncrud.common.template;
    
    import com.likelion.likelioncrud.common.error.ErrorCode;
    import com.likelion.likelioncrud.common.error.SuccessCode;
    import lombok.*;
    
    @Getter // getter 메소드 자동 생성 lombok 어노테이션
    @AllArgsConstructor(access = AccessLevel.PRIVATE) // 모든 필드 값을 매개변수로 받는 생성자 자동 생성
    @RequiredArgsConstructor(access = AccessLevel.PRIVATE) // final 붙은 필드만 매개변수로 받는 생성자 자동 생성
    @Builder // 빌더 패턴
    public class ApiResTemplate<T> {
    
        private final int code;       // 응답 코드 (200, 404 등)
        private final String message; // 응답 메시지
        private T data;               // 응답 데이터 (제네릭 형식으로 다양한 타입 수용 가능)
    
        // 데이터 없는 성공 응답
        public static ApiResTemplate successWithNoContent(SuccessCode successCode) {
            return new ApiResTemplate<>(successCode.getHttpStatusCode(), successCode.getMessage());
        }
    
        // 데이터 포함한 성공 응답
        public static <T> ApiResTemplate<T> successResponse(SuccessCode successCode, T data) {
            return new ApiResTemplate<>(successCode.getHttpStatusCode(), successCode.getMessage(), data);
        }
    
        // 에러 응답 (커스텀 메시지 포함)
        public static ApiResTemplate errorResponse(ErrorCode errorCode, String customMessage) {
            return new ApiResTemplate<>(errorCode.getHttpStatusCode(), customMessage);
        }
    
    }
    

CustomExceptionAdvice : 전역 예외 처리기

커스텀 예외 클래스를 정의하고 이를 처리

모든 컨트롤러에서 발생하는 예외를 한 곳에서 처리

  • CustomExceptionAdvice(하영님 예외처리 강의자료 참고)
    package com.likelion.likelioncrud.common.exception;
    
    import com.likelion.likelioncrud.common.error.ErrorCode;
    import com.likelion.likelioncrud.common.exception.BusinessException;
    import com.likelion.likelioncrud.common.template.ApiResTemplate;
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.stereotype.Component;
    import org.springframework.web.bind.annotation.*;
    
    @Slf4j // 로깅을 위한 Logger를 생성
    @RestControllerAdvice // REST API 컨트롤러에 대한 예외 처리 어드바이스임을 나타내는 어노테이션
    @Component // 클래스를 Spring 컴포넌트로 등록
    @RequiredArgsConstructor
    public class CustomExceptionAdvice {
    
        /**
         * 500 Internal Server Error
         * 원인 모를 이유의 예외 발생 시
         * 모든 종류의 Exception 처리
         */
        @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // HTTP 500 상태 코드 반환 어노테이션
        @ExceptionHandler(Exception.class)  // 모든 종류의 Exception 처리
        public ApiResTemplate handleServerException(final Exception e) {
            log.error("Internal Server Error: {}", e.getMessage(), e); // 로그 출력
            return ApiResTemplate.errorResponse(ErrorCode.INTERNAL_SERVER_ERROR, e.getMessage());
        }
    
        /**
         * custom error
         * 내부 커스텀 에러
         * BusinessException 처리
         */
        @ExceptionHandler(BusinessException.class)
        public ResponseEntity<ApiResTemplate> handleCustomException(BusinessException e) {
            log.error("CustomException: {}", e.getMessage(), e); // 로그 출력
    
            ApiResTemplate<?> body = ApiResTemplate.errorResponse(e.getErrorCode(), e.getMessage());
           
            // 에러 코드에 정의된 HTTP 상태 코드로 응답
            return ResponseEntity
                    .status(e.getErrorCode().getHttpStatus())
                    .body(body);
        }
    
    }
    

자기참조 관계(순환관계)

엔티티의 하나의 데이터가 같은 엔티티의 다른 데이터와 관계를 가지는 것

즉, 엔티티가 자기 자신과 관계를 가지는 것

EX)

학생의 학부모가 한 명 있다고 가정하면 위 사진처럼 관계를 맺는다

하지만 학생이 자라서 학부모가 된다면 위 모델은 수용할 수 없다

그러면 위 사진처럼 학생 밑에 새로운 엔티티를 추가하면 해결이 가능하다

하지만 학생_1이 다시 자라 학부모가 된다면 이 모델은 다시 수용할 수 없어진다

수용하려면 다시 엔티티를 추가해야하는데 좋은 모델이란 엔티티의 최소한의 변경으로 변화에 대응해야 한다

이럴 때 사용하는 것이 자기참조관계이다

학부모와 학생은 모두 사람이기에 통합하고 자기참조관계선을 추가해 학생의 학부모가 누구인지 관리할 수 있다

여기서 필수적인 관계로 만들면 최상위 부모 개체가 만들어 질 수 없기에 Null값을 허용한 관계선을 설정해야 한다

다대다(N:M) 관계

@ManyToMany에 대하여

JPA에서는 두 엔티티 간의 다대다 관계를 맺을 때 @ManyToMany 어노테이션을 사용할 수 있다

하지만 실무에서는 이를 직접 사용하는 것을 지양한다 →

  • 실무에서는 다대다 관계를 단순 연결로 끝내지 않고 추가 정보를 연결 테이블에 넣는 경우가 많다
  • 예를 들어 회원과 상품 간의 다대다 관계를 생각해보면 우리는 주문 이라는 중간 테이블을 만들어 사용한다
  • 이 주문 테이블에는 단순히 회원 ID, 상품 ID만 있는 것이 아니라, 주문 수량, 주문 일시, 상태 같은 추가 정보도 함께 저장되어야 한다
  • 그런데 @ManyToMany를 그대로 사용하면 JPA는 중간 테이블을 엔티티로 만들지 않고 단순 연결만하기에 이런 추가 정보를 다룰 수 없다

⇒ 따라서 다대다 관계를 일대다 - 다대일 관계로 풀어서 구현한다

  • 중간 테이블을 명시적인 엔티티로 만든다
  • 그 엔티티 내부에서 @ManyToOne, @OneToMany 를 사용해 각 엔티티를 연결한다

따라서 @ManyToMany 를 직접 사용하지 않고 중간 엔티티(PostTag)를 만들어

  • 게시글(Post)게시글-태그(PostTag) (일대다)
  • 태그(Tag)게시글-태그(PostTag) (일대다)

로 나누어 구현해보겠습니다~

라이브 코딩 실습

멋쟁이 사자처럼 강의 자료와 운영진에게 질문을 통해 실습을 해 보아요~

기존 crud와 예외처리, 폼 검증을 하던 프로젝트에 이어서 진행할게요!

오늘의 테스트 목표

post 조회 시 tag들을 함께 출력

PostTag라는 중간 엔티티를 이용해 다대다 관계를 설정

crud 강의와 동일하게 tag의 저장, 전체 조회, 단건 조회, 수정, 삭제

→ 오늘 목표를 강의 시간에 다 만드려니 시간이 너무 짧죠??

그래서 오늘 강의 시간 목표는 따로 있고, 나머지 목표들은 과제로 해 주시면 됩니다!

오늘의 목표를 완성하신 분들은 귀가하시면 됩니다~ 그러니 운영진들에게 질문하며 진행해 주세요! 지피티는 사용하지 않기!!


오늘의 강의시간 목표

게시글(Post)에는 여러 개의 태그(Tag)를 달 수 있습니다!

예를 들어 “Spring”, “CRUD”같은 태그를 붙일 수 있겠죠?

이런 구조는 다대다 관계이지만 우리는 Post와 Tag사이에 PostTag라는 중간 엔티티를 사용하여 설계하겠습니다!!

  • Tag엔티티와 PostTag 엔티티를 작성해 주세요

  • Post엔티티는 이미 CRUD 강의 시간에 작성했으니 그 코드에 Post와의 연관관계를 추가하시면 됩니다

  • <힌트>

    Ex)

    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    • 아무런 매개변수가 없는 생성자를 생성하되 같은 패키지나 하위 클래스에서만 접근 가능

    • JPA가 클래스의 기본 생성자를 호출해서 객체를 만들고 그 위에 프록시를 씌워 지연 로딩을 가능하게 함

    • AllArgsConstructor, RequiredArgsConstructor

      ```java
      @AllArgsConstructor // 모든 필드 값을 파라미터로 받는 생성자를 생성
      @RequiredArgsConstructor // final이나 @NonNull으로 선언된 필드만을 파라미터로 받는 생성자를 생성
      ```
      @Id // 기본 키(PK)를 지정
      @GeneratedValue(strategy = GenerationType.IDENTITY)
    • GeneratedValue 어노테이션은 기본 키 값을 자동 생성

    • straregy의 속성은 4가지
      - AUTH : 특정 DB에 맞게 자동 선택(기본값)
      - IDENTITY : 기본 키 생성을 데이터베이스에 위임(ex.MYSQL등)
      - SEQUENCE : 데이터베이스 시퀀스를 사용해 기본키 할당(ex.ORACLE, H2등)
      - TABLE : 키 생성 전용 테이블을 만들어 SEQUENCE처럼 사용(ex. 모든 DBMS)

      @Column(name = "member_id", nullable = false, length = 10)
    • Column어노테이션의 속성
      - name : DB 컬럼명 지정
      - nullable : null 허용 여부
      - length : 문자 길이 제한

      @ManyToOne(fetch = FetchType.LAZY)
    • fetch = FetchType.Lazy → 지연 로딩 : 연관 관계를 즉시 로딩하지 않고 실제로 사용할 때 DB에서 조회 (성능 최적화)

    • 연관된 엔티티를 프록시 객체로 생성해 두었다가 실제 접근이 일어날 때 DB에서 로딩

    • 프록시 : 실제 엔티티 객체 대신 데이터베이스 조회를 지연할 수 있는 가짜 객체

    • 즉시 로딩

      @ManyToOne(fetch = FetchType.EAGER)
    • 즉시 로딩을 사용하게 되면 예를 들어 연관된 엔티티의 데이터를 수만 건 등록했을 때, 연간 된 엔티티를 로딩하는 수간 수만 건의 데이터가 함께 로딩될 것 → 그렇기에 연관된 엔티티가 하나면 즉시 로딩을, 아니라면 지연 로딩을 사용

    • ManyToOne, OneToMany에서는 지연 로딩이 권장됨

      @JoinColumn(name = "member_id")
    • JoinColumn은 엔티티의 연관관계에서 외래 키를 매핑하기 위해 사용됨

    • 내가 외래 키를 가지고 있을 때, 그 외래 키가 어떤 컬럼으로 매핑될지 지정함

    • name속성은 매핑할 외래 키의 이름을 지정함

    • +속성

      • nullable : 외래 키의 NULL 허용 여부
      • unique : 유일성 제약 여부
    • 결론적으로 참조하는 엔티티가 존재할 경우 JoinColumn으로 FK를 매핑하는 필드의 이름과 대상 엔티티의 PK로 FK 이름이 지정됨

    • JoinColumn은 외래 키를 소유하는 쪽에서 사용, 반대편에서는 mappedBy 사용

    • 외래 키를 직접 소유 (vs mappedBy)

      @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
    • mappedBy = "post" → PostTag 엔티티의 post 필드가 연관관계의 주인

    • cascade = ALL → 게시글 저장/삭제 시 PostTag도 함께 처리됨

    • orphanRemoval = true → Post에서 PostTag 제거 시 DB에서도 삭제됨

Post 엔티티

  • 하나의 Member는 여러 Post와 연결되어야 한다(N:1)
  • 하나의 Post는 여러 PostTag와 연결되어야 한다(1:N)
  • postId는 PK이며 자동 증가되어야 한다
  • title, contents는 반드시 존재해야 한다
  • Member는 반드시 있어야 하며 지연로딩으로 설정해야 한다
  • PostTag는 Post와 함께 저장, 삭제 된다
  • 과제 시 참고
    package com.likelion.likelioncrud.post.domain;
    
    import com.likelion.likelioncrud.member.domain.Member;
    import com.likelion.likelioncrud.post.api.dto.request.PostUpdateRequestDto;
    import com.likelion.likelioncrud.posttag.domain.PostTag;
    import jakarta.persistence.*;
    import lombok.AccessLevel;
    import lombok.Builder;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    
    import java.util.ArrayList;
    import java.util.List;
    
    @Entity
    @Getter
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    public class Post {
    
        @Id
        @Column(name = "post_id")
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long postId;
    
        @Column(nullable = false)
        private String title;
    
        @Column(nullable = false)
        private String contents;
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "member_id", nullable = false)
        private Member member;
    
        // 일대다 관계 하나의 게시글은 여러 PostTag와 연결 가능
        @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
        private List<PostTag> postTags = new ArrayList<>();
    
        @Builder
        private Post(String title, String contents, Member member) {
            this.title = title;
            this.contents = contents;
            this.member = member;
        }
    
        public void update(PostUpdateRequestDto postUpdateRequestDto) {
            this.title = postUpdateRequestDto.title();
            this.contents = postUpdateRequestDto.contents();
        }
    }

Tag 엔티티

  • 하나의 Tag는 여러 PostTag와 연결되어야 한다(1:N)
  • tagId는 기본키이며, 자동 생성되어야 한다
  • name 속성을 가지며 반드시 존재해야 한다
  • PostTag는 Tag와 함께 저장, 삭제 된다
  • 과제 시 참고
    package com.likelion.likelioncrud.tag.domain;
    
    import com.likelion.likelioncrud.posttag.domain.PostTag;
    import com.likelion.likelioncrud.tag.api.dto.request.TagUpdateRequestDto;
    import jakarta.persistence.*;
    import lombok.AccessLevel;
    import lombok.Builder;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    
    import java.util.ArrayList;
    import java.util.List;
    
    @Entity
    @Getter
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    public class Tag {
    
        @Id
        @Column(name = "tag_id")
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        @Column(nullable = false)
        private String name;
    
        // PostTag와의 일대다 연관관계
        @OneToMany(mappedBy = "tag", cascade = CascadeType.ALL, orphanRemoval = true)
        private List<PostTag> postTags = new ArrayList<>();
    
        @Builder
        public Tag(String name) {
            this.name = name;
        }
    
        public void update(TagUpdateRequestDto tagUpdateRequestDto) {
            this.name = tagUpdateRequestDto.name();
        }
    }
    

PostTag 엔티티

  • 여러 PostTag는 하나의 Post에 연결되어야 한다(N:1)
  • 여러 PostTag는 하나의 Tag에 연결되어야 한다(N:1)
  • postTagId는 PK이며 자동 생성되어야 한다
  • post는 FK(외래키)이름을 지정해주고 반드시 존재해야하며 지연로딩을 설정해야 한다
  • tag는 FK(외래키)이름을 지정해주고 반드시 존재해야하며 지연로딩을 설정해야 한다
  • 과제 시 참고
    package com.likelion.likelioncrud.posttag.domain;
    
    import com.likelion.likelioncrud.post.domain.Post;
    import jakarta.persistence.*;
    import lombok.AccessLevel;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    import com.likelion.likelioncrud.tag.domain.Tag;
    
    @Entity
    @Getter
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    public class PostTag {
    
        @Id
        @Column(name = "post_tag_id")
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        // 다대일 관계, 여러 PostTag가 하나의 Post에 연결 가능
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "post_id", nullable = false)
        private Post post;
    
        // 다대일 관계, 여러 PostTag가 하나의 Tag에 연결 가능
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "tag_id", nullable = false)
        private Tag tag;
    
        public PostTag(Post post, Tag tag) {
            this.post = post;
            this.tag = tag;
        }
    }
    

0개의 댓글