@Transactional@Transactional을 붙여주면 해당 메서드는 트랜잭션으로 관리된다@Transactional 속성readOnly: 읽기 전용(CUD 불가), 성능 최적화propagation: 트랜잭션 전파 규칙 정의 (REQUIRED, REQUIRES_NEW 등)isolation: 트랜잭션 격리 수준 설정 (동시성 문제 제어)rollbackFor: 특정 예외 상황에 강제 롤백 지정입문 주차에선
readOnly속성만 알아본다.
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
/**
* 회원가입 (새 데이터 추가 - readOnly = false)
*/
@Transactional
public Long signUp(String email) {
// 이메일 중복 검사 등의 로직이 포함될 수 있음
Member member = new Member(email);
memberRepository.save(member);
return member.getId();
}
/**
* 회원 조회 (데이터 변경이 없는 읽기 작업)
* readOnly = true 속성으로 성능을 최적화합니다.
*/
@Transactional(readOnly = true)
public Member findMemberByEmail(String email) {
return memberRepository.findByEmail(email)
.orElseThrow(() -> new IllegalArgumentException("해당 이메일의 유저가 없습니다."));
}
}
save(entity)findById, findAll 등delete(entity), deleteById, deleteAll프로젝트 세팅은 이 글의 실습 탭 참조

User) 생성id를 제외한 필드만 초기화하는 생성자 필요 (@AllArgsConstructor 사용하면 안됨!)id는 @GeneratedValue(strategy = GenerationType.IDENTITY)에 따라 자동 생성되기 때문에, 임의로 값을 설정해선 안됨update 메서드 구현 필요@Getter
@Entity
@Table(name = "users")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 50, nullable = false)
private String name;
@Column(unique = true, nullable = false)
private String email;
private String address;
public User(String name, String email, String address) {
this.name = name;
this.email = email;
this.address = address;
}
public void update(String name, String email, String address) { // 추후 Update 작업에 필요
this.name = name;
this.email = email;
this.address = address;
}
}
UserRepository) 생성public interface UserRepository extends JpaRepository<User, Long> {
// 내부를 비워도 JpaRepository가 갖고 있는 save(), findById(), ... 등의 메서드 사용 가능
// 커스텀 쿼리 메서드 정의 가능
// 예: Optional<User> findByEmail(String email);
}
UserService) 생성UserRepository를 필드로 가지는 것이 자연스러운 설계@Service
@RequiredArgsConstructor // final 필드인 userRepository를 초기화하는 생성자 필요
public class UserService {
private final UserRepository userRepository;
}
UserController) 생성UserService를 필드로 가지는 것이 자연스러운 설계@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
}
UserService 클래스에 CRUD 작업 구현3 Layer Architecture에 의해 Controller는 User 엔티티를 직접 참조하면 안된다!
- Entity는 DB의 테이블과 1:1로 매핑되는 객체이므로, 상위 계층인 Controller에서는 직접 참조하지 않고, Service 레이어와 Repository 레이어에서만 접근할 수 있다.
- Controller와 Service 레이어 간에는 DTO를 통해 데이터를 주고받아야 한다.
- Service는 요청 DTO를 가지고 Entity를 만들어 Repository에 DB CRUD 연산을 요청한 뒤, 결과를 다시 응답 DTO로 만들어 반환한다.
CreateUserRequest
id 필드가 없다. id는 요청에 필요한 정보가 아니라, DB에서 자동 생성되는 정보이다.final 키워드가 없다.final 키워드를 사용하면 안된다.@Getter
public class CreateUserRequest {
public String name;
public String email;
public String address;
}
CreateUserResponse
final로 설정된다.@Getter
@RequiredArgsConstructor
public class CreateUserResponse {
private final Long id;
private final String name;
private final String email;
private final String address;
}
@Getter
public class CreateUserResponse {
private final Long id;
private final String name;
private final String email;
private final String address;
// Entity를 직접 전달받아 필드 매핑
public CreateUserResponse(User user) {
this.id = user.getId();
this.name = user.getName();
this.email = user.getEmail();
this.address = user.getAddress();
}
}
from이 예약어 같은 게 아닌 단순 메서드 이름이고 GetUserResponse는 단순히 메서드 리턴 타입이다. (Unity 개발에선 메서드를 PascalCase로 쓰는 것이 일반적이다보니 이런 부분에서 가끔 혼동이 온다.)@Getter
@RequiredArgsConstructor
public class GetUserResponse {
private final Long id;
private final String name;
private final String email;
private final String address;
public static GetUserResponse from(User user) {
return new GetUserResponse(
user.getId(),
user.getName(),
user.getEmail(),
user.getAddress()
);
}
}
@Transactional
public CreateUserResponse save (CreateUserRequest request) {
User user = new User(
request.getName(),
request.getEmail(),
request.getAddress()
);
User savedUser = userRepository.save(user);
return CreateUserResponse.from(savedUser);
}
현재 구현에서 `GET` '요청'에는 DTO가 필요하지 않으니 작성하지 않아도 된다.
GetUserResponse
@Getter
@RequiredArgsConstructor
public class GetUserResponse {
private final Long id;
private final String name;
private final String email;
private final String address;
public static GetUserResponse from(User user) {
return new GetUserResponse(
user.getId(),
user.getName(),
user.getEmail(),
user.getAddress()
);
}
}
// 전체 조회
@Transactional
public List<GetUserResponse> getAll() {
List<User> userList = userRepository.findAll();
List<GetUserResponse> dtoList = new ArrayList<>();
for (User user : userList) {
dtoList.add(GetUserResponse.from(user));
}
return dtoList;
}
// 단 건 조회
@Transactional
public GetUserResponse getOne(Long userId) {
User user = userRepository.findById(userId).orElseThrow(
() -> new IllegalStateException("User not found.")
);
return GetUserResponse.from(user);
}
UpdateUserRequest
@Getter
public class UpdateUserRequest {
private String name;
private String email;
private String address;
}
UpdateUserResponse
@Getter
@RequiredArgsConstructor
public class UpdateUserResponse {
private final Long id;
private final String name;
private final String email;
private final String address;
public static UpdateUserResponse from(User user) {
return new UpdateUserResponse(
user.getId(),
user.getName(),
user.getEmail(),
user.getAddress()
);
}
}
@Transactional
public UpdateUserResponse update(Long userId, UpdateUserRequest request) {
User user = userRepository.findById(userId).orElseThrow(
() -> new IllegalStateException("User not found.")
);
user.update(
request.getName(),
request.getEmail(),
request.getAddress()
);
return UpdateUserResponse.from(user);
}
userId를 받아 삭제하는 메서드로, DTO가 필요하지 않다.@Transactional
public void delete(Long userId) {
if (!userRepository.existsById(userId)) {
throw new IllegalStateException("User not found.");
}
userRepository.deleteById(userId);
}
package org.example.jpapractice.service;
import lombok.RequiredArgsConstructor;
import org.example.jpapractice.dto.*;
import org.example.jpapractice.entity.User;
import org.example.jpapractice.repository.UserRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Transactional
public CreateUserResponse save (CreateUserRequest request) {
User user = new User(
request.getName(),
request.getEmail(),
request.getAddress()
);
User savedUser = userRepository.save(user);
return CreateUserResponse.from(savedUser);
}
@Transactional
public List<GetUserResponse> getAll() {
List<User> userList = userRepository.findAll();
List<GetUserResponse> dtoList = new ArrayList<>();
for (User user : userList) {
dtoList.add(GetUserResponse.from(user));
}
return dtoList;
}
@Transactional
public GetUserResponse getOne(Long userId) {
User user = userRepository.findById(userId).orElseThrow(
() -> new IllegalStateException("User not found.")
);
return GetUserResponse.from(user);
}
@Transactional
public UpdateUserResponse update(Long userId, UpdateUserRequest request) {
User user = userRepository.findById(userId).orElseThrow(
() -> new IllegalStateException("User not found.")
);
user.update(
request.getName(),
request.getEmail(),
request.getAddress()
);
return UpdateUserResponse.from(user);
}
@Transactional
public void delete(Long userId) {
if (!userRepository.existsById(userId)) {
throw new IllegalStateException("User not found.");
}
userRepository.deleteById(userId);
}
}
ResponseEntity로 감싸준다.ResponseEntity는 HTTP 응답의 상태 코드, 헤더, 본문을 직접 제어하고 관리할 수 있도록 도와주는 클래스이다.@PostMapping("/users") // RESTful API
public ResponseEntity<CreateUserResponse> create(@RequestBody CreateUserRequest request) {
CreateUserResponse result = userService.save(request);
return ResponseEntity.status(HttpStatus.CREATED).body(result);
}
// 전체 조회
@GetMapping("/users")
public ResponseEntity<List<GetUserResponse>> getAll() {
List<GetUserResponse> result = userService.getAll();
return ResponseEntity.status(HttpStatus.OK).body(result);
}
// 단 건 조회
@GetMapping("/users/{userId}")
public ResponseEntity<GetUserResponse> getOne(@PathVariable Long userId) {
GetUserResponse result = userService.getOne(userId);
return ResponseEntity.status(HttpStatus.OK).body(result);
}
@PutMapping("/users/{userId}")
public ResponseEntity<UpdateUserResponse> update(
@PathVariable Long userId, @RequestBody UpdateUserRequest request) {
UpdateUserResponse result = userService.update(userId, request);
return ResponseEntity.status(HttpStatus.OK).body(result);
}
@DeleteMapping("/users/{userId}")
public ResponseEntity<Void> delete(@PathVariable Long userId) {
userService.delete(userId);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
package org.example.jpapractice.controller;
import lombok.RequiredArgsConstructor;
import org.example.jpapractice.dto.*;
import org.example.jpapractice.service.UserService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping("/users")
public ResponseEntity<CreateUserResponse> create(@RequestBody CreateUserRequest request) {
CreateUserResponse result = userService.save(request);
return ResponseEntity.status(HttpStatus.CREATED).body(result);
}
@GetMapping("/users")
public ResponseEntity<List<GetUserResponse>> getAll() {
List<GetUserResponse> result = userService.getAll();
return ResponseEntity.status(HttpStatus.OK).body(result);
}
@GetMapping("/users/{userId}")
public ResponseEntity<GetUserResponse> getOne(@PathVariable Long userId) {
GetUserResponse result = userService.getOne(userId);
return ResponseEntity.status(HttpStatus.OK).body(result);
}
@PutMapping("/users/{userId}")
public ResponseEntity<UpdateUserResponse> update(
@PathVariable Long userId, @RequestBody UpdateUserRequest request) {
UpdateUserResponse result = userService.update(userId, request);
return ResponseEntity.status(HttpStatus.OK).body(result);
}
@DeleteMapping("/users/{userId}")
public ResponseEntity<Void> delete(@PathVariable Long userId) {
userService.delete(userId);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
}
![]() | ![]() |
|---|


![]() | ![]() |
|---|






모든 요청에 대해 올바르게 동작하였다.
상태 코드 역시ResponseEntity에서 설정한 대로 잘 응답하는 것을 확인하였다.
본 실습에서는 바텀업 방식으로 개발하였다. (Entity -> Service -> Controller)
IntelliJ에서는 탑다운 방식으로 보다 빠르게 작업할 수 있다. (Controller 먼저 구현, Service에 아직 정의되지 않은 메서드를 IDE에서 생성하도록 요청하면 기본 틀을 잡아줌)