2025-04-14
Controller, Service, Repository)Repository에서 SQL을 직접 작성하였다.JPA (Java Persistence API)
자바 진영의 ORM
(Object-Relation Mapping)
JPA - 규칙
Hibernate - 내부에 JDBC를 사용하고 있다.
@Entity@Entity
public class User
{
private long id;
private String name;
private Integer age;
...
}
@Id, @GeneratedValue@Id : 이 필드를 PRIMARY KEY로 간주한다.@GeneratedValue : PRIMARY KEY는 자동 생성되는 값이다. (AUTO_INCREMENT)@Entity
public class User
{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
protected User()
{
// 기본 생성자
}
}
protected로 매개 변수 없이 생성할 수 있다.@Column@Column : 객체의 필드와 Table의 필드를 매핑한다.@Column 어노테이션 생략 가능하다.@Entity
public class User {
@Column(nullable = false, length = 20, name = "user_name")
private String name;
private Integer age; // @Column 생략 가능
}
jpa:
hibernate:
ddl-auto: none
properties:
hibernate:
format_sql: true
show_sql: true
dialect: org.hibernate.dialect.MySQL8Dialect
ddl-auto스프링이 시작할 때 DB에 있는 테이블과 객체의 필드가 다를 경우 어떻게 처리할지에 대한 것
create : 기존 테이블이 있다면 삭제 후 다시 생성create-drop : 스프링이 종료될 때 테이블을 모두 제거update : 객체와 테이블이 다른 부분만 변경validate : 객체와 테이블이 동일한지 확인none : 별다른 조치를 하지 않는다.format_sqlshow_sqldialectUserJdbcRepository 클래스명 변경UserRepository 인터페이스 생성JpaRepository 상속public interface UserRepository extends JpaRepository<User, Long>
{
}
JpaRepository를 상속받기 때문에 @Repository 어노테이션을 생략 해도 된다.<User> : 이 Repository가 관리할 엔티티 클래스<Long> : User의 PK 타입 (보통 @Id가 붙은 필드 타입)Service
// [INSERT]
public void insertUser(UserCreateRequest request)
{
User user
= userRepository.save(new User(request.getName(), request.getAge()));
}
userRepository (JpaRepository 상속) 를 통해 데이터를 INSERT 할 수 있다.Service
private List<UserResponse> selectUser()
{
List<User> userList = userRepository.findAll();
return userList.stream()
.map(user -> new UserResponse(user.getId(), user.getName(), user.getAge()))
.collect(Collectors.toList());
}
함수 반환 타입
여기서 함수 반환 타입이 List<User> 가 아닌 <UserResponse> 인 이유는 <User>는 DB와 연결된 객체이고, 사용자에게 보여지기 위해서는 DB에 직접 연결된 <User> 보다는 <UserResponse>를 통해 한번 감싸서 보여지는 것이 보안상 더 안전하기 때문이다.
findAll()
: 자동으로 SQL을 날려서 해당 테이블에 있는 모든 데이터를 가져온다. (SELECT * FROM user)
↪ 그렇게 가져온 정보는 List가 된다. 여기서 List는 DB에 접근한 객체이기 때문에 <User> 타입이다.
RETURN
⑴ 그렇게 만들어진 userList를 stream()을 통해서 모든 데이터에 접근한다.
⑵ map()을 통해 List의 <User> 타입을 함수 반환 타입인 <UserResponse> 타입으로 변환한다.
⑶ User 객체의 id, name, age 값을 꺼내서(get 메서드) UserResponse 객체를 생성한다. (= DTO 로 변환한다.)
stream(): 리스트 같은 데이터를 한줄씩 꺼내면서, 편하게 가공할 수 있는 파이프 라인 (반복문을 더 깔끔하게 만들었다고 보면 됨)map(): 각 User 객체를 DTO로 변환한다. 즉, 스트림 내부의 데이터를 다른 타입으로 변환한다.collect(): 스트림을 다시 묶어서 반환Collectors.toList(): 리스트로 묶는다.
// [UserResponse 생성자]
public UserResponse(User user)
{
this.id = user.getId();
this.name = user.getName();
this.age = user.getAge();
}
// [Service]
return userList.stream()
.map(UserResponse::new)
.collect(Collectors.toList());
UserResponse::new : 생성자 참조user -> new UserResponse(user)Service
public void updateUser(UserUpdateRequest request)
{
// 1) id 존재 여부 판단
User user = userRepository.findById(request.getId())
.orElseThrow(IllegalArgumentException::new);
// 2) uddate
user.updateName(request.getName());
userRepository.save(user); // save() 메서드를 호출한다. -> 자동으로 UPDATE SQL이 실행됨
}
id 존재 여부 판단
findById() : Id 에 해당하는 user를 조회한다. (SELECT * FROM user WEHRE user_id = ?)
Id에 해당하는 user가 있다면 user 객체에 저장
Id에 해당하는 user가 없다면 예외 발생
update
save() → 자동으로 UPDATE SQL이 실행됨
① save() : 주어지는 객체를 저장하거나 업데이트 시켜준다.
② findAll() : 주어지는 객체가 매핑된 테이블의 모든 데이터를 가져온다. (SELECT * FROM user)
③ findById() : Id를 기준으로 특정한 1개의 데이터를 가져온다. (SELECT * FROM user WEHRE user_id = ?)
Spring Data JPA
복잡한 JPA 코드를 스프링과 함께 쉽게 사용할 수 있도록 도와주는 라이브러리
save() | findAll() | findById()
Spring Data JPA > JPA > Hibernate(JPA 구현체) > JDBC
⇒ Spring Data JPA는 JDBC를 사용하기 때문에 SQL을 작성하지 않아도 동작할 수 있다.
UserRepository
public interface UserRepository extends JpaRepository<User, Long>
{
User findByName(String name);
}
find만 작성하면, 1개의 데이터만 가져온다.By 뒤에 붙는 필드 이름으로 SELECT 쿼리의 WHERE 문이 작성된다.public void deleteUser(String name)
{
// SELECT * FROM user WHERE name = ?
// name을 찾아서 해당 user가 존재하면 user 객체 생성
// 해당 user가 없으면 null 반환
User user = userRepository.findByName(name);
if (user == null)
throw new IllegalArgumentException();
userRepository.delete(user);
}
userRepository.delete() : 주어지는 데이터를 DB에서 제거한다. (DELETE SQL)find : 1건을 가져온다.
반환 타입 - 객체 or Optional<T>
예시) User user = findByName(String name);
→ Name에 해당하는 user 객체를 찾는다.
findAll : 쿼리의 결과물이 N개인 경우에 사용한다.
반환 타입 - List<T>
예시) List<User> userList = userRepository.findAll();
→ user 테이블의 모든 정보를 반환하여 리스트에 저장
exists : 쿼리 결과가 존재하는지 확인한다.
반환 타입 - boolean
예시) existsByAge()
→ Age에 해당하는 회원의 존재 여부를 반환한다.
count : SQL의 결과 개수를 센다.
반환 타입 - long
예시) countByAge(Integer age)
→ 해당 Age를 가진 user의 수를 반환한다.
AND or OR로 조합할 수 있다.
List<User> userList = findAllByNameAndAge(String name, Integer age);
↪ SELECT * FROM user WHERE user_name= ? AND age = ?
GreaterThan : 초과
GreaterThanEqual : 이상
LessThan : 미만
LessThanEqual : 이하
Between : 사이에
예시) List<User userList = findAllByBetween(int startAge, int endAge);
↪ SELECT * FROM user WHERE user_age BETWEEN ? AND ?;
StartWith : ~로 시작하는EndsWith : ~로 끝나는START TRANSACTION;COMMIT;ROLLBACK;START TRANSACTION;INSERT한 결과가 B 단말기에서 보이지 않는다.COMMMIT;COMMIT;을 실행해야 B 단말기에서도 결과를 볼 수 있다.ROLLBACK;ROLLBACK;을 실행하면 A, B 단말기 모두 결과를 볼 수 없다.START TRANSACTION;COMMIT;ROLLBACK;@Transactional@Transactional 어노테이션의 아래에 있는 함수가 시작될 때 → START TRANSACTION;COMMIT;ROLLBACK;// [INSERT]
@Transactional
public void insertUser(UserCreateRequest request)
{
User user = userRepository.save(new User(request.getName(), request.getAge()));
}
// [SELECT]
@Transactional(readOnly = true)
public List<UserResponse> selectUsers()
{
List<User> userList = userRepository.findAll();
return userList.stream() // 현재 List는 stream 형태이므로 collect()를 통해 리스트로 변환한다.
.map(user -> new UserResponse(user.getId(), user.getName(), user.getAge()))
.collect(Collectors.toList());
}
@Transactional(readOnly = true)SELECT 쿼리만 사용한다면, readOnly 옵션을 사용할 수 있다.IOException과 같은 Checked Exception은 예외가 발생해도 ROLLBACK;이 일어나지 않는다.Entity 객체를 관리/보관하는 역할변경 감지 (Dirty Check)
→ 영속성 컨텍스트 안에서 불러와진 Entity는 명시적으로 save하지 않더라도, 변경을 감지해 자동으로 저장된다.
쓰기 지연
→ INSERT, UPDATE, DELETE 시, 객체를 세 개 저장할 때, DB와 세 번 통신하는 것이 아니라, 영속성 컨텍스트가 세 개의 객체를 기억해뒀다가 한 번에 DB에 저장하여 총 한 번 통신하도록 한다.
1차 캐싱
→ ID를 기준으로 Entity를 기억한다.
→ SELECT 시 동일한 데이터를 여러번 조회할 경우, 한 번 조회했던 데이터를 계속 사용할 수 있다.
→ 캐싱된 객체(영속성 컨텍스트가 잠깐 저장한 객체)는 완전히 동일하다. (주소까지)