@RestController, @Controller
- 모두 스프링 프레임워크에서
웹 요청을 처리하는 클래스에 사용하는 애너테이션
- 반환 방식과 목적이 다름
String
이면 뷰(View) 이름으로 처리됨@ResponseBody
를 메서드마다
붙여야 함: 웹 페이지 렌더링(템플릿, HTML) - Thymeleaf, JSP
@Controller
public class HomeController {
@GetMapping("/hello")
public String hello(Model model) {
model.addAttribute("msg", "안녕하세요!");
return "hello"; // → templates/hello.html 렌더링
}
@GetMapping("/json")
@ResponseBody
public String json() {
return "데이터 응답"; // → HTTP Body에 그대로 응답
}
}
@ResponseBody
를 포함함 --> 모든 메서드가 JSON
으로 반환됨:: REST API 서비스 (데이터 전달 중심) - Vue, React
@RestController
public class ApiController {
@GetMapping("/api/hello")
public String hello() {
return "안녕하세요!"; // → {"message":"안녕하세요!"} 형태로 JSON 응답
}
@GetMapping("/api/user")
public User getUser() {
return new User("홍길동", 30); // → 객체가 자동으로 JSON 변환되어 반환
}
}
@PathVariable, @RequestParam, @RequestBody
- 모두 스프링 컨트롤러에서
HTTP 요청의 데이터를 받아오는 방식
- 데이터의 위치와 사용 목적이 다름
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id);
}
:: 명확한 리소스 지정 (GET)
@GetMapping("/search")
public List<User> search(@RequestParam String keyword, @RequestParam(defaultValue = "1") int page) {
return userService.search(keyword, page);
}
:: 선택적 조건 (GET)
@PostMapping("/users")
public User createUser(@RequestBody UserDTO userDto) {
return userService.save(userDto);
}
// 호출 예
POST /users
Content-Type: application/json
{
"name": "홍길동",
"age": 30
}
응답의 모든 요소(HTTP 상태 코드, 헤더, 바디)
를 세밀하게 제어할 수 있기 때문
--> REST API를 만들 때 클라이언트와의 통신 규약을명확하고 일관성 있게 유지
하는 데 매우 중요한 도구
ResponseEntity<T>
는 스프링 프레임워크에서 제공하는 HTTP 응답 전체를 구성할 수 있는 객체
@RestController + 객체반환
만으로 충분하지만,상태 코드 조정이나 헤더 설정
이 필요한 경우엔 반드시 ResponseEntity 사용Response DTO
와 함께 사용됨return new ResponseEntity<>("등록 성공", HttpStatus.CREATED);
→ 클라이언트가 요청이 성공했는지, 실패했는지를 정확한 HTTP 코드로 구분 가능
HttpHeaders headers = new HttpHeaders();
headers.add("X-Custom-Header", "테스트");
return new ResponseEntity<>(userDto, headers, HttpStatus.OK);
→ 클라이언트에게 커스텀 헤더
를 포함한 정교한 응답 전송 가능
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("잘못된 요청입니다."));
→ 모든 에러 응답을 같은 포맷(JSON 구조 등)으로 맞출 수 있어, 프론트엔드 처리 편리
return ResponseEntity.ok(userDto); // 200 OK
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("사용자를 찾을 수 없습니다");
→ 코드가 간결하고 읽기 쉬움
DI (Dependency Injection, 의존성 주입)
- 스프링이 가진 가장 강력한 기능 중 하나
객체 간의 의존 관계를 직접 생성하지 않고
, 외부(주로 스프링 컨테이너)에서 주입해주는 방식
// 직접 생성하는 방식 (X - 강한 결합)
UserService service = new UserService(new UserRepository());
// 의존성 주입 방식 (O - 약한 결합)
@Autowired
UserService service;
[HTTP 요청]
↓
[Controller] → 외부 요청 처리, 요청 DTO ↔ 응답 DTO
↓
[Service] → 비즈니스 로직, 트랜잭션 처리
↓
[Repository] → DB 접근, JPA or SQL 실행
계층 | 애너테이션 | 역할 |
---|---|---|
Controller | @RestController , @Controller | HTTP 요청/응답 처리, 외부 통신 |
Service | @Service | 비즈니스 로직 수행 |
Repository | @Repository | 데이터 접근, DB와 직접 통신 (JPA, MyBatis 등) |
@Repository
public class UserRepository {
public User findById(Long id) {
// DB 조회 로직
}
}
@Service
public class UserService {
private final UserRepository userRepository;
// 생성자 주입
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUser(Long id) {
return userRepository.findById(id);
}
}
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
// 생성자 주입
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userService.getUser(id));
}
}
@Autowired
는 생성자에 생략 가능 (Spring 4.3+)@Service
public class UserService {
@Autowired
private UserRepository userRepository;
// 직접 접근 불가 → 테스트 시 주입 어려움
}
ReflectionTestUtils
등으로 주입해야 하므로 매우 불편@Service
public class UserService {
private final UserRepository userRepository;
// Spring 4.3 이후에는 @Autowired 생략 가능
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
final
사용 가능 → 불변성 보장@Service
public class NotificationService {
private EmailSender emailSender;
@Autowired
public void setEmailSender(EmailSender emailSender) {
this.emailSender = emailSender;
}
}
장점
단점
- 애플리케이션의 구조를 더 견고하고 유연하게 유지하기 위해
계층 간 책임 분리(Separation of Concerns)
와 보안, 유지보수성 측면에서 매우 중요- Entity를 그대로 반환하면
Jackson 순환 참조 문제(StackOverflowError)
가 생길 수 있음
--> DTO로 해결 가능!
ModelMapper, MapStruct 등의 라이브러리를 사용하면 Entity ↔ DTO 간 변환을 자동화할 수 있어 생산성이 향상됨
Entity 를 그대로 API 로 반환하면 민감 정보(PW, 내부 식별자 등)가 그대로 노출될 수 있음
--> DTO를 사용하면 API 요구사항에 맞게 유연하게 응답 구조 조정 가능!
// 필요 없는 필드도 모두 로딩됨
return userRepository.findAll(); // Entity 직접 반환 ❌
// 필요한 필드만 선택적으로 응답
return userService.getUserList(); // DTO 반환 ⭕
--> 역할을 분리하면 테스트, 유지보수, 리팩토링이 훨씬 수월해짐
@Transactional
- Spring에서 데이터베이스 트랜잭션을 관리할 수 있도록 해주는 핵심 애너테이션
- 비즈니스 로직이 실행되는 동안 트랜잭션을 시작하고, 예외가 발생하면 롤백하며, 정상적으로 끝나면 커밋되도록 함
@Transactional
은 프록시 기반 AOP를 통해 동작함checked 예외
도 롤백하고 싶다면 다음처럼 설정@Transactional(rollbackFor = Exception.class)
private
/protected
메서드에는 동작하지 않음public void outer() {
inner(); // @Transactional이어도 적용되지 않음
}
반드시 다른 클래스를 통해 호출돼야 트랜잭션이 적용됩니다.
- 단순 조회 쿼리에는
@Transactional(readOnly = true)
를 사용하면 성능이 향상 (스냅샷, flush 등 최소화)@Transactional
은 Service 계층에만 붙이는 것이 가장 이상적입니다 (비즈니스 로직 책임이 집중되는 곳)- 트랜잭션 범위를 좁게 설정해서 장시간 락 점유를 피하세요. (예: 외부 API 호출은 트랜잭션 안에서 하지 말 것)
영속성 컨텍스트(Persistence Context)
- JPA(Java Persistence API)에서 가장 중요한 개념 중 하나
- 이 개념을 이해해야 엔티티의 생명주기, 1차 캐시, 변경 감지, 지연 로딩 등 JPA의 핵심 기능을 정확히 다룰 수 있음
엔티티(Entity) 객체들을 관리하는 일종의 메모리(캐시) 공간
User user1 = em.find(User.class, 1L); // DB 조회
User user2 = em.find(User.class, 1L); // 캐시에서 조회됨 (쿼리 없음)
→ 같은 트랜잭션 내에서 user1 == user2
는 true
불필요한 DB 조회를 방지함으로써 성능 향상
같은 엔티티 ID로 여러 번 조회해도 DB 접근 없이 캐시된 객체 반환
user1.setName("변경됨");
em.flush(); // 자동으로 UPDATE 발생
→ 같은 ID의 엔티티는 하나만 존재하므로, 같은 참조로 값 변경 추적 가능
@Transactional
public void updateName(Long id, String name) {
User user = em.find(User.class, id);
user.setName(name); // UPDATE 쿼리 자동 생성됨
}
명시적으로 em.merge()
호출할 필요 없음
User user = new User(); // 비영속
em.persist(user); // 영속
em.detach(user); // 준영속
em.remove(user); // 삭제
- 성능 최적화에서 반드시 고려해야 할 핵심 이슈
- 연관 관계를 조회할 때 Lazy 로딩 전략과 함께 사용하면 무심코 발생
1개의 쿼리를 실행했는데, 추가적으로 N개의 쿼리가 발생하는 상황을 말함
List<Team> teams = teamRepository.findAll(); // 1번 쿼리 (팀 전체 조회)
for (Team team : teams) {
System.out.println(team.getMembers()); // N번 쿼리 (각 팀의 멤버 조회)
}
teams
가 5개라면?@Query("SELECT t FROM Team t JOIN FETCH t.members")
List<Team> findAllWithMembers();
JOIN FETCH
를 사용하면 한 번의 쿼리로 연관된 엔티티까지 한꺼번에 가져옵니다.
SELECT t.*, m.*
FROM team t
JOIN member m ON m.team_id = t.id
@EntityGraph(attributePaths = {"members"})
@Query("SELECT t FROM Team t")
List<Team> findAllWithMembers();
@EntityGraph
는 지연 로딩 설정을 무시하고 명시된 연관 관계를 즉시 로딩합니다.
여러 건을 한 번에 조회할 수 있게 IN 쿼리로 묶어서 실행
spring.jpa.properties.hibernate.default_batch_fetch_size=100
Hibernate는 내부적으로 IN 쿼리로 최적화
→ SELECT * FROM member WHERE team_id IN (?, ?, ?, ...)
단점: 완전한 해결책은 아니고 임시 완화용
@Query("SELECT new com.example.TeamDto(t.name, m.name) FROM Team t JOIN t.members m")
List<TeamDto> findTeamDtos();
필요한 필드만 조회하여 과도한 엔티티 로딩 방지
Fetch Join + DTO 프로젝션 혼합 전략
이 유효OneToMany
)에 Fetch Join은 페이징 불가!LAZY
를 쓰되, 필요한 곳에서만 명시적으로 JOIN FETCH 또는 EntityGraph를 쓰는 게 베스트 프랙티스임EAGER
전략으로 바꾸는 것은 권장하지 않습니다.