@Entity
@Data
@NoArgsConstructor
public class User extends AuditableEntity {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "user_id", insertable = false, updatable = false) // 읽기 전용
private List<Post> posts = new ArrayList<>();; // post.userId = user.id
}
@Entity
@Data
@NoArgsConstructor
public class Post extends AuditableEntity{
@Id
@GeneratedValue
private Long id;
private String content;
@Column(name = "user_id")
private Long userId; // 연관 객체 대신 외래키 값만 저장
}
단방향 @OneToMany에서 @JoinColumn을 사용하는 이유
문제점: Join Table이 자동 생성됨
JPA에서 @OneToMany 단방향을 사용할 때 @JoinColumn을 명시하지 않으면,
JPA는 중간에 조인 테이블 (예: user_post) 를 생성해서 User와 Post를 연결.
import jakarta.persistence.*;
import java.time.LocalDateTime;
public class AuditListener {
@PrePersist
public void perPersist(Object o) {
if (o instanceof Auditable) {
((Auditable) o).setCreateAt(LocalDateTime.now());
((Auditable) o).setUpdateAt(LocalDateTime.now());
}
}
@PreUpdate
public void perUpdate(Object o) {
if (o instanceof Auditable) {
((Auditable) o).setUpdateAt(LocalDateTime.now());
}
}
}
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@NoArgsConstructor
@Data
@EntityListeners(AuditListener.class)
public class Post implements Auditable {
@Id
@GeneratedValue
private Long id;
private String title;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// getter/setter 생략 가능
}
AuditingEntityListener
Spring Boot에서 제공하는 AuditingEntityListener는 Spring Data JPA의 감사(Auditing) 기능을 자동화해주는 리스너 클래스다. 이 리스너를 통해 createdDate, lastModifiedDate, createdBy, lastModifiedBy 같은 필드를 자동으로 관리할 수 있다.
ORM은 Object Relational Mapping의 약자로, 객체 지향 프로그래밍 언어(예: Java)에서 사용하는 객체(엔티티)와 관계형 데이터베이스(RDB)에서 사용하는 테이블 간의 불일치를 해결하고, 이 둘을 자동으로 매핑해주는 기술입니다. 즉, 자바의 객체와 데이터베이스의 테이블을 1:1로 연결해주는 중계자 역할을 하며, 개발자가 객체를 조작하면 그에 맞춰 자동으로 데이터베이스에 반영되도록 해줍니다
@GeneratedValue는 JPA(Java Persistence API)에서 엔티티 클래스의 주요 키(Primary Key) 값을 자동으로 생성하기 위해 사용하는 어노테이션입니다. 주로 @Id와 함께 사용되어, 데이터베이스에 엔티티를 저장할 때 해당 필드의 값을 자동으로 생성하도록 지시합니다
JpaRepository<{Entity 클래스} , {PK 데이터 타입}> 을 상속 받는다.
Application.yml 설정
spring:
jpa:
show-sql: true #jpa 사용 시 sql 로그 출력
properties:
hibernate:
format_sql: true # sql 로그를 sql 포맷형식으로 출력
defer-datasource-initialization: true
h2:
console:
enabled: true
Spring Boot 2.5 이상부터는 기본적으로 data.sql과 같은 SQL 스크립트가 Hibernate(JPA)의 DDL(테이블 생성 등) 작업보다 먼저 실행된다. 이 때문에 Hibernate가 아직 초기화 되지 않았는데 data.sql이 실행되어 에러가 발생할 수 있다.
spring.jpa.defer-datasource-initialization: true를 설정하면 Hibernate의 DDL 작업(엔티티 기반 테이블 생성 등)이 모두 끝난 뒤에 data.sql, schema.sql 등 SQL 스크립트가 실행된다
JPA 주요 메소드 및 네이밍 규칙 정리
JpaRepository가 기본적으로 제공하는 주요 메소드와 Spring Data JPA 쿼리 메소드 네이밍 규칙을 아래와 같이 정리할 수 있습니다.
// 콘솔 결과
pages :Page 2 of 2 containing com.example.bookmanager.domain.User instances
totalElements :5
totalPages :2
Number of elements :2
sort :UNSORTED
size : 3
QueryByExampleExecutor란?
QueryByExampleExecutor는 Spring Data JPA에서 제공하는 인터페이스로, 엔티티의 일부 필드 값(예: name, email 등)을 예시로 삼아 동적 쿼리(Example Query)를 쉽게 생성할 수 있게 해줍니다. 즉, “이런 값이 들어있는 엔티티를 찾아줘”라는 식의 검색을 코드 몇 줄로 구현할 수 있다.
위 방식은 특히 문자열에 관련된 것만 쓸 수 있다는 한계점이 있어서 조금 복잡한 쿼리를 만들 때는
package com.example.client.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserResponse {
private String name;
private int age;
}
package com.example.client.service;
import com.example.client.dto.UserResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
@Service
public class RestTemplateService {
// <http://localhost/api/server/hello>
// response
public UserResponse hello(){
URI uri = UriComponentsBuilder
.fromUriString("<http://localhost:9090>")
.path("/api/server/hello")
.queryParam("name","jun")
.queryParam("age",30)
.encode()
.build()
.toUri();
System.out.println(uri.toString());
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<UserResponse> result = restTemplate.getForEntity(uri, UserResponse.class);
System.out.println("getStatusCode : "+ result.getStatusCode());
System.out.println("getBody : "+ result.getBody());
return result.getBody();
}
}
RestTemplate 클래스를 이용하여 다른 서버로 요청(GET, POST, DELETE, …) 을 보낼 수 있다.
서버와 API 표준 스펙을 정하여 DTO 클래스를 설계하여 응답을 받을 때 사용한다.
Server
package com.example.server.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String name;
private int age;
}
package com.example.server.controller;
import com.example.server.dto.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/server")
public class ServerApiController {
@GetMapping("/hello")
public User hello(@RequestParam String name ,@RequestParam int age){
User user = new User();
user.setName(name);
user.setAge(age);
return user;
}
}
2. Server to Server - POST
Client
package com.example.client.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserRequest {
private String name;
private int age;
}
package com.example.client.service;
import com.example.client.dto.UserRequest;
import com.example.client.dto.UserResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
@Service
public class RestTemplateService {
public UserResponse post(){
// <http://localhost:9090/api/server/user/{userId}/name/{userName}>
URI uri = UriComponentsBuilder
.fromUriString("<http://localhost:9090>")
.path("/api/server/user/{userId}/name/{userName}")
.encode()
.build()
.expand(100,"jun")
.toUri();
System.out.println(uri);
// http body -> object -> object mapper -> json -> rest template -> http body json
UserRequest req = new UserRequest();
req.setAge(30);
req.setName("jun");
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<UserResponse> response = restTemplate.postForEntity(uri, req, UserResponse.class);
System.out.println("getStatusCode : "+ response.getStatusCode());
System.out.println("getHeaders : "+ response.getHeaders());
System.out.println("getBody : "+ response.getBody());
return response.getBody();
}
}
RestTemplate의 postForEntity 함수를 이용하여 requstBody의 내용을 추가하여 다른 서버로 요청할 수 있다.
Server
package com.example.server.controller;
import com.example.server.dto.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("/api/server")
public class ServerApiController {
@PostMapping("/user/{userId}/name/{userName}")
public User post(@RequestBody User user, @PathVariable int userId, @PathVariable String userName){
log.info("userId : {} , userName : {}",userId,userName);
log.info("user : {}",user);
return user;
}
}