이전 JPA 기본 정리에서 추가로 들어간 부분만 모아 정리.
첨부 소스(AppStart.java,BaseTime.java,MovieEntity.java,MovieDto.java,MovieRepository.java,MovieService.java,MovieController.java)의 구조를 예시 코드에 반영.
@Entity // 클래스 ↔ 테이블 매핑
@Table(name = "movie") // 실제 테이블명 지정
public class MovieEntity { // PK, 컬럼 등은 아래 예시에서 상세
...
}
@Entity @Id @Table(name="...") @GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValued가 아니라 @GeneratedValue SEQUENCE 등을 사용하기도 함@Column nullable = true/false : NOT NULL 제약unique = true/false : 단일 컬럼 UNIQUE 제약(복합 유니크는 @Table(uniqueConstraints=...))name = "컬럼명" : 실제 컬럼명length = n : 문자열 길이insertable = true/false : INSERT SQL 생성 시 포함 여부updatable = true/false : UPDATE SQL 생성 시 포함 여부MovieEntity (추가 포인트 포함)@Entity
@Table(name = "movie")
public class MovieEntity extends BaseTime {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 120)
private String title;
@Column(name = "director", length = 60, nullable = false)
private String director;
@Column(unique = true, length = 20)
private String code; // 비즈니스 키 (예: 영화코드). 단일 unique 예시
@Column(updatable = false)
private String createdBy; // insert 시에만 반영하고 이후 수정 금지
// 기본 생성자, getter/setter ...
}
1) AppStart (메인 클래스) 위에 @EnableJpaAuditing 추가
@SpringBootApplication
@EnableJpaAuditing
public class AppStart {
public static void main(String[] args) {
SpringApplication.run(AppStart.class, args);
}
}
2) BaseTime 추상 클래스(공통 감사 필드)
@Getter
@MappedSuperclass // 상속 엔티티에 필드를 매핑하도록 함
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTime {
@CreatedDate // INSERT 시 자동 주입
private LocalDateTime createdAt;
@LastModifiedDate // UPDATE 시 자동 갱신
private LocalDateTime updatedAt;
}
3) 감사 필드를 쓰고 싶은 엔티티가 BaseTime 상속
@Entity
@Table(name = "movie")
public class MovieEntity extends BaseTime {
...
}
🔎 추가 팁
@CreatedBy,@LastModifiedBy등 작성자/수정자도 AuditorAware를 구현하면 자동 주입 가능@Column(updatable=false)와 조합해 “최초 생성자/생성일 불변” 정책 구현에 활용
Controller ⇄ DTO Service ⇄ Entity View(JSON) ←→ DTO ←→ Controller ←→ DTO ←→ Service ←→ Entity ←→ Repository(JPA)
public class MovieDto {
private Long id;
private String title;
private String director;
private String code;
public MovieEntity toEntity() {
MovieEntity e = new MovieEntity();
// id는 보통 신규생성 시 null
e.setTitle(this.title);
e.setDirector(this.director);
e.setCode(this.code);
return e;
}
}
@Entity
@Table(name = "movie")
public class MovieEntity extends BaseTime {
// id, title, director, code, createdAt, updatedAt ...
public MovieDto toDto() {
MovieDto dto = new MovieDto();
dto.setId(this.id);
dto.setTitle(this.title);
dto.setDirector(this.director);
dto.setCode(this.code);
// 필요 시 감사 필드도 전달
// dto.setCreatedAt(this.getCreatedAt());
// dto.setUpdatedAt(this.getUpdatedAt());
return dto;
}
}
Repository
@Repository
public interface MovieRepository extends JpaRepository<MovieEntity, Long> {
Optional<MovieEntity> findByCode(String code);
}
Service
@Service
@RequiredArgsConstructor
public class MovieService {
private final MovieRepository movieRepository;
public MovieDto create(MovieDto dto) {
MovieEntity saved = movieRepository.save(dto.toEntity());
return saved.toDto();
}
public MovieDto readByCode(String code) {
MovieEntity e = movieRepository.findByCode(code)
.orElseThrow(() -> new IllegalArgumentException("영화코드 없음"));
return e.toDto();
}
@Transactional
public MovieDto update(Long id, MovieDto dto) {
MovieEntity e = movieRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("영화 없음"));
e.setTitle(dto.getTitle());
e.setDirector(dto.getDirector());
e.setCode(dto.getCode());
return e.toDto(); // Dirty Checking으로 UPDATE 자동 반영
}
}
Controller
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/movies")
public class MovieController {
private final MovieService movieService;
@PostMapping
public ResponseEntity<MovieDto> create(@RequestBody MovieDto dto) {
return ResponseEntity.ok(movieService.create(dto));
}
@GetMapping("/code/{code}")
public ResponseEntity<MovieDto> readByCode(@PathVariable String code) {
return ResponseEntity.ok(movieService.readByCode(code));
}
@PutMapping("/{id}")
public ResponseEntity<MovieDto> update(@PathVariable Long id, @RequestBody MovieDto dto) {
return ResponseEntity.ok(movieService.update(id, dto));
}
}
✅ 중요 주의사항
- Controller에서는 Entity를 직접 반환하지 않는다.
(엔티티 구조 노출·순환 참조·지연 로딩 문제 등 부작용 방지)- DTO로 변환해 필요한 데이터만 주고받을 것
@GeneratedValue(strategy = GenerationType.IDENTITY) 는 MySQL에서 주로 사용 (AUTO_INCREMENT) @Column(insertable=false, updatable=false) 는 DB 또는 트리거가 채우는 컬럼에 유용 @EnableJpaAuditing 을 메인 클래스에 반드시 선언 @MappedSuperclass + @EntityListeners(AuditingEntityListener.class) 로 구성 @GeneratedValue, @Column의 insertable/updatable 등)@EnableJpaAuditing, BaseTime)