🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [52]일차
🚀52일차에는 RESTful API 에 대해 배워보고 @RestController어노테이션을 통해
웹에서 보여지는 것을 실습해볼 수 있었다. 또한 DB와의 연결을 통해 Service, Controller를 설계할 수 있었다.
그 전 JOIN, EAGER, LAZY 등 Fetch 종류, 트랜잭션의 개념에 대해 공부하였다.
학습 목표 : RESTful API를 통해 DB와 연결 및 아키텍처 설계하기
학습 과정 : 회고를 통해 작성
조인 JOIN
- DB에서 두 개이상의 테이블을 연결하여 데이터를 조회하는 방법
내부조인 INNER JOIN
- 가장 일반적인 형태로 두 테이블의 “교집합”만을 결과로 반환
- 즉 두 테이블 간 일치하는 데이터의 해당 데이터만 표시
@Query("SELECT e FROM Employee e JOIN e.department d WHERE d.name = :departmentName")
List<Employee> findEmployeesByDepartmentName(@Param("departmentName") String departmentName);
외부조인 OUTER JOIN
- 한 테이블의 값이 다른 테이블에 일치하는 값이 없어도 해당 데이터를 포함하여 결과 반환
- LEFT OUTER JOIN :왼쪽 테이블의 모든 데이터와 오른쪽 테이블의 일치하는 데이터를 반환
- RIGHT OUTER JOIN : 오른쪽 테이블의 모든 데이터와 왼쪽 테이블의 일치하는 데이터를 반환
- FULL OUTER JOIN : 양쪽 테이블의 모든 데이터를 반환하며 일치하는 데이터가 없을 경우에는 NULL로 채움
- 크로스 JOIN (CORSS JOIN) : 두 테이블 간의 “카테시안 곱”을 반환. 이는 첫번쨰 테이블의 모든 행이 두번째 테이블의 모든 행과 결합됨 WHERE절이나 다른 형태의 필터링 없이 사용되며 매우 큰 결과의 집합을 내보낼 수 있음
- 자연 JOIN (NATURAL JOIN) : 두 테이블 간 이름이 같은 모든 컬럼에 대해 암묵적인 INNER JOIN 실시 이름이 같은 컬럼을 기준으로 JOIN 을 수행하는 것
외부조인 예시
List<Employee> findByDepartmentIdInAndSalaryBetween(
List<Integer> departmentIds, Double minSalary, Double maxSalary);
select * from employees e1_0
left join departments d1_0
on d1_0.department_id=e1_0.department_id
where d1_0.department_id in (?) and e1_0.salary between ? and ?
- 위의 코드는 내부적으로 LEFT JOIN 이 수행되고 있음
- LEFT JOIN을 통해 왼쪽 테이블(=employees 테이블)의 모든 행과
오른쪽 테이블(=departments 테이블)의 일치하는 행을 반환 - 일치하지 않는 경우, 오른쪽 테이블의 결과는 NULL로 채워지게 됨
Fetch 종류
EAGER vs LAZY
- JPA(Java Persistence API) 에서 엔티티 간 관계를 로드하는 방법을 제어하는 Fetch (페치) 전략
LAZY Fetching - 지연 로딩
- 관계된 엔티티를 실제 필요할때까진 로드하지 않음
- 초기 로딩 시 불필요한 데이터를 로드하지 않아 성능 최적화에 유리
- 하지만 관련 엔티티에 접근할때마다 추가쿼리를 발생시킬 수 있어 View계층에서 데이터 접근시 LazyInitializationException 이 발생할 수 있음 (=트랜잭션 범위 밖에서 지연 로딩된 엔티티에 접근 시)
LAZY Fetching 예시
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Department department;
EAGER Fetching - 즉시 로딩
- 엔티티 로드 시 관계된 엔티티도 함께 로드
➡️즉 부모 엔티티 조회 시 관련된 모든 자식 엔티티또한 DB에서 즉시 가져옴 - 한번의 쿼리에서 모든 관련 데이터를 가져올 수 있기때문에
지연로딩에서 발생할 수 있는 다수의 쿼리실행을 피할 수 있음 - 하지만 초기 로딩 시 불필요한 데이터를 모두 로드하기떄문에 불필요한 데이터 로드에 시간이 많이 소요될 수 있음
Fetch 기본값
@ManyToOne : 기본적으로 EAGER 로딩
@OneToOne : 기본적으로 EAGER 로딩
@OneToMany : 기본적으로 LAZY 로딩
@ManyToMany : 기본적으로 LAZY 로딩
❓페치 조인 (Fetch JOIN) 과 1 + N 문제 해결 방법
- 페치 조인 : JPQL에서 사용되는 조건 중 하나로 한번의 쿼리로 여러 엔티티를 함께 가져오는 방법
➡️여러 엔티티를 한번에 가져오기때문에 성능 최적화에 유용
특히 EAGER 로딩을 통해 불필요한 여러 쿼리가 실행되는 것을 방지 가능
1 + N 문제
EmployeeRepository 메소드 추가
List<Employee> findBySalaryBetween(Double minSalary, Double maxSalary);
// 1 + N 문제
log.info("Employees in department 1 with salary between 2900 and 3100:");
employeeRepository.findBySalaryBetween(2900.0, 3100.0)
.forEach(employee -> log.info(employee.toString()));
- findBySalaryBetween()은 단순히 salary 범위에 맞는 Employee 목록을 가져오는 메서드
- 이때 department나 job이 Lazy Loading으로 설정되어 있으면,
각 Employee 객체를 가져올 때마다 추가적인 쿼리(N개)가 발생할 수 있음 - 즉, 한 번 메인 쿼리가 불러와질때마다 N번의 추가 쿼리가 실행될 수 있음
✅1번 해결방법
@Query("SELECT e FROM Employee e JOIN FETCH e.department d WHERE d.id IN :departmentIds AND e.salary BETWEEN :minSalary AND :maxSalary")
List<Employee> findByDepartmentIdInAndSalaryBetween(@Param("departmentIds") List<Integer> departmentIds,
@Param("minSalary") Double minSalary,
@Param("maxSalary") Double maxSalary);
- JOIN FETCH를 사용하여 한 번 메인 쿼리가 불러와질때마다
이 Employee와 연관된 Department 데이터를 함께 가져오게 설계 - Lazy Loading 문제를 방지할 수 있어서 추가적인 쿼리 문제를 줄임
SELECT e.*, d.*
FROM employees e
JOIN departments d ON e.department_id = d.department_id
WHERE d.department_id IN (30)
AND e.salary BETWEEN 2900 AND 3100;
- 내부적으로는 이러한 쿼리가 실행될 것
- 한 번의 쿼리로 Employee + Department 데이터를 함께 조회
정리하자면
1+N문제를 Fetch JOIN으로 해결할 수 있는 것
Fetch JOIN 으로 연관된 엔티티를 즉시 로딩 (EAGER LOADING)하여 해결하는 것
⚠️주의할 점으로 중복데이터가 발생할 수 있고, 여러 개의 Fetch JOIN은 사용이 불가능함
✅출력결과 : Employee 객체들이 toString()되지 않고 가져와지고 있는 문제
⚠️toString()의 결과로 제대로 출력되지 않는 것 해결방법
[Spring Data JPA] 여러 관계가 매핑된 엔티티의 toString() 메소드 출력 오류
상황hr데이터베이스에 설계된 엔티티들을 생성하면서 많은 엔티티와 관계를 갖고 있는 Employee엔티티를 만들게되었는데 이 Employee 엔티티를 CommandLineRunner에서 findBySalaryBetween() 메소드를 테스트
lefton.tistory.com
Service Layer 서비스 계층
- Service레이어는 애플리케이션의 비즈니스 로직을 처리하는 계층으로
컨트롤러와 데이터 접근 계층 (DAO/Repository) 사이에서 중재역할 - 역할:
- 비즈니스 로직 집중화 : 모든 비즈니스 로직은 Service레이어에서 처리 ➡️코드 재사용성, 유지보수 용이
- 트랜잭션 관리 : 트랜잭션 경계를 정의하는 레이어이므로 하나의 서비스 메소드가 하나의 트랜잭션으로 처리
- 컨트롤러와 분리 : 컨트롤러는 요청을 받고 응답을 반환하는 역할만 수행하며,
비즈니스 로직은 Service레이어에서 처리됨. 이를 통해 역할을 명확하게 구분가능
트랜잭션 처리
- 트랜잭션은 DB 일관성, 무결성을 유지하기 위해 여러 작업을 하나의 단위로 묶어주는 기능
- Spring에서는 @Transactional 어노테이션으로 트랜잭션을 관리함
트랜잭션 전파 (Propagation)
- 메소드 간 트랜잭션 동작 방식 정의
- 기본값은 REQUIRED : 현재 트랜잭션이 존재하면 이를 사용하고, 없으면 새로운 트랜잭션을 시작
- ex. Service레이어와 Repository레이어에서 트랜잭션을 어떻게 어디까지 가져가야할지 설정 (전파레벨 변경)
- 💡ex. Service레이어에서 @Transactional 이 붙은 Service메소드 안에서
Repository메소드(ex. userRepository.save(user);)를 호출할때,
이 Repository도 Service레이어에서 시작된 하나의 트랜잭션 안에서 처리된다는 것
➡️Service에서 만들어진 트랜잭션이 Repository로 전파되었다고 할 수 있다.
트랜잭션 전파 레벨 종류
- REQUIRED (기본값) : 현재 트랜잭션 존재 시 이를 사용, 없으면 새로운 트랜잭션 시작
- REQUIRES_NEW : 항상 새로운 트랜잭션 시작, 기존 트랜잭션이 존재했을 경우 기존 트랜잭션을 일시중지
- SUPPORTS : REQUIRED와 달리 없으면 트랜잭션 없이 실행함
- NOT_SUPPORTED : 항상 트랜잭션 없이 실행, 기존 트랜잭션이 존재했을 경우 기존 트랜잭션을 일시중지
- MANDATORY : 반드시 기존 트랜잭션 내에서 실행되어야하고,
기존 트랜잭션이 존재하지 않을 경우에는 예외발생 - NEVER : 트랜잭션 없이 실행되어야하고, 존재하면 예외발생
- NESTED : 현재 트랜잭션 내에서 중첩 트랜잭션을 시작
중첩된 트랜잭션은 부모트랜잭션과는 독립적으로 커밋, 롤백이 가능하다.
트랜잭션 전파 레벨 설정방법
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createUserWithNewTransaction(User user) {
// 항상 새로운 트랜잭션을 시작
}
트랜잭션 격리 레벨 (Isolation Level)
- "동시에 실행되는 트랜잭션 간" 데이터 처리 방식 정의
- Spring에서는 다양한 격리 수준을 제공함 (기본값 DEFAULT)
- ex. 격리 수준은 A사용자가 COMMIT()을 하지 않았는데 B사용자가 접근이 가능한 격리수준의 DB,
A사용자가 COMMIT()을 하지 않으면 B사용자도 접근이 불가능하도록 격리수준을 지정하는 DB도 있다.
트랜잭션 격리 레벨 종류
- DEFAULT (기본값) : DB 벤더의 기본 격리 수준을 따름 (MySQL이면 MySQL규칙에 맞게)
- READ_UNCOMMITTED : 다른 트랜잭션이 커밋하지 않은 데이터를 읽을 수 있게됨 (Dirty Read 허용)
- READ_COMMITTED : 다른 트랜잭션이 커밋한 데이터만 읽을 수 있음 (Dirty Read 방지)
- REPEATABLE_READ : 트랜잭션이 시작된 시점의 데이터를 읽으며 다른 트랜잭션이 변경한 데이터를 읽지 않음 (Non-repeatable Read 방지)
- SERIALIZABLE : 가장 엄격한 수준으로 트랜잭션이 순차적으로 실행되는 것처럼 동작 (Phantom Read 방지)
트랜잭션 격리 레벨 설정방법
@Transactional(isolation = Isolation.SERIALIZABLE)
public void createUserWithSerializableIsolation(User user) {
// 가장 엄격한 격리 수준으로 트랜잭션 실행
}
트랜잭션 경계 설정
- 트랜잭션이 끝나고 시작되는 지점을 정의
- Spring에서는 @Transactional 어노테이션을 사용하여 메소드 단위로 트랜잭션 경계를 설정할 수 있음
- 트랜잭션 동작과정 @Transactional 선언된 메소드 호출 시
트랜잭션 시작 메소드 내 모든 DB작업은 하나의 트랜잭션 내에서 처리되고
메소드가 정상적으로 종료되면 트랜잭션이 커밋되고, 예외가 발생시 트랜잭션이 롤백
@Service
public class UserService{
@Transactional
public void createUser(User user){
// 트랜잭션 시작
// 비즈니스 로직 처리
// 트랜잭션 종료
}
}
OSiV 패턴
- Open Session In View
- 웹 애플리케이션에서 Hibernate같은 ORM 프레임워크 사용 시
DB 세션(or 엔티티 매니저)을 뷰 렌더링까지 유지하는 패턴 - 뷰 레이어에서 지연 로딩(Lazy Loading)을 사용할 수 있음
OSiV 패턴 동작 과정
- 요청 수신 : 클라이언트의 요청을 서버가 수신
- 세션 시작 : 요청 처리하는 동안 DB 세션(or 엔티티매니저)가 열림
- 서비스 및 리포지토리 호출 : 서비스, 리포지토리 계층에서 DB 작업 수행
- 지연 로딩 허용 : DB 세션이 열려있기때문에 지연 로딩이 가능
- 뷰 렌더링 : DB세션이 열린 상태로 뷰를 렌더링
- 세션 종료 : 요청처리가 끝난 후 DB 세션이 닫힘
➡️즉 뷰를 렌더링 한 후 DB 세션을 닫는 것
➡️컨트롤러 레이어에서부터 DB 세션을 열린 상태로 사용하여 뷰가 닫힌 후에야 DB 세션을 닫는 것
➡️만약 DB세션이 아닌 EntityManager라고 가정하면 영속성 컨텍스트가 계속 열린 상태로 사용된다는 것
OSiV 패턴의 단점
- 성능 문제 : DB 세션이 뷰 렌더링까지 열려있는 것으로 요청 처리시간이 길어져
DB리소스 사용을 증가시켜 성능문제가 발생할 수 있음 - 트랜잭션 경계 모호화 : 트랜잭션 경계가 명확하지 않음
(비즈니스 로직과 뷰 렌더링이 동일한 트랜잭션 경계를 갖게됨 >> 예기치 않은 데이터 변경이 발생할 수 있음) - 지연 로딩 남용 : 지연 로딩 남용시 DB쿼리가 뷰 레이어에서 발생하여 성능 저하를 초래할 수 있음
OSiV 패턴 비활성화
- SpringBoot에서 OSiV패턴을 비활성화할 수 있음
spring:
jpa:
open-in-view=false
- application.yml 설정파일에 open-in-view=false 추가
- OSiV는 true가 기본값이고 트랜잭션을 무작정 길게 가져가지 않도록 false를 해줄 수 있다는 것
Rest API
- ❓@Controller와 @RestController의 차이점
➡️@Controller
SpringMVC의 기본 컨트롤러 어노테이션, HTTP요청을 처리하는 역할, 주로 뷰 템플릿을 반환
데이터를 뷰로 전달하고 뷰를 렌더링할 수 있음 반환된 뷰는 ViewResolver로 사용자에게 보여질 페이지를 생성함
➡️@RestController
@Controller와 @ResponseBody가 결합, RESTful 웹 서비스를 쉽게 만들 수 있도록 돕는 어노테이션
HTTP 요청을 처리하고 데이터를 JSON이나 XML형태로 클라이언트에게 “직접 반환” - @RestController가 적용된 메소드는 HttpMessageConverter를 사용하여
반환된 객체를 HTTP응답 본문에 직접 쓰게됨
이 어노테이션은 Rest API 개발시 주로 사용되며 클라이언트에게 데이터 모델 “자체를 반환”
Restful
- HTTP와 URI 기반으로 자원에 접근할 수 있도록 제공하는 애플리케이션 개발 “인터페이스”
- 이를 통해 개발자는 HTTP메소드와 URI만으로 웹 데이터를 CRUD할수 있게됨
- URI (ex. http://localhost:8080/employees) ➡️employees라는 자원에 접근하는 것
- ex. 게시글 id가 1번인 게시글을 Restful 로 나타내면
http://localhost:8080/boards/1
HTTP 방식
- RESTful 방식과 달리 HTTP 방식은 자원이 아닌 메소드로 나타냄
➡️메소드 : GET / POST / PUT / PATCH / DELETE 등 - http://localhost:8080/boards/1 ➡️ GET방식이면 이 ID가 1번인 게시글을 읽기 (READ)
- http://localhost:8080/boards/1 ➡️ POST방식이면 ID가 1번인 게시글에 쓰기 (CREATE)
- http://localhost:8080/boards/1 ➡️ PATCH나 PUT방식이면 ID가 1번인 게시글을 수정 (UPDATE)
- http://localhost:8080/boards/1 ➡️ DELETE방식이면 ID가 1번인 게시글을 삭제 (DELETE)
HTTP 방식은 URL은 변하지 않고 메소드들만 바뀌는 것
🚀자원을 URI로 표시하고 이 자원들을 어떤 방식으로 처리할 것인지를 나타내는 것이 HTTP 방식
🚀자원의 위치나 식별자를 URI로 바로 알아볼 수 있게 만드는 약속을 RESTful 방식
▶️실습 - Rest API
Controller클래스 생성
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "[인사] 안녕하세요.";
}
}
- http://localhost:8080/hello 접속
➡️출력 : [인사] 안녕하세요. - @Controller를 통해 “뷰”로 반환할 것인지 (=ViewResolver를 통해 뷰를 렌더링)
- @RestController를 통해 “데이터”로 반환할 것인지의 차이점 (=뷰 렌더링 없이 데이터 자체를 반환)
HttpMessageConverter
- HTTP메시지를 자바에서 인식가능한 JSON 객체로 바꿔주는 역할
- @RestController 메소드의 리턴타입으로는 Object 처럼 모든 자바객체가 들어올 수 있음
(=자바의 어느 값이라도 담아서 리턴해줄 수 있기때문) - JSON변환과정은 Spring 프레임워크의 HttpMessageConverter인터페이스를 통해 이루어짐
- 특히 JSON변환의 경우에는
MappingJackson2HttpMessageConverter (Jackson 라이브러리 사용)가 일반적으로 사용됨 - 이 컨버터는 자바 객체를 JSON으로, JSON을 자바 객체로변환하는 역할을 수행
- Spring MVC가 HTTP 요청 본문을 자바 객체로 변환하거나 자바 객체를 HTTP 응답 본문으로 변환할때 HttpMessageConverter를 사용
@RestController
public class MyRestController {
@GetMapping("/api/greeting")
public Map<String, String> greet(){
Map<String, String> res = new HashMap<>();
res.put("message", "안녕하세요.");
res.put("key", "[key]에 따른 VALUE!");
res.put("Cookie", "오레오!");
return res;
}
}
- Map<String, String> 처럼 Map 객체도 반환할 수 있음
- localhost:8080/api/greeting 접속 시 JSON 데이터가 보여짐
▶️실습 - Rest API : User 추가
@Getter@Setter
public class User {
private String name;
private String phoneNumber;
private String address;
public User(String name, String phoneNumber, String address) {
this.name = name;
this.phoneNumber = phoneNumber;
this.address = address;
}
}
@GetMapping("/api/user")
public User getUser(){
return new User("Isak", "010-1111-1111", "타인위어");
}
- localhost:8080/api/user 접속 시 JSON 데이터가 보여짐
@RequestParam 기본값
@GetMapping("/api/user/param")
public User getUserParam(@RequestParam(name="name") String name){
return new User(name, "010-1111-1111", "타인위어");
}
- @RequestParam을 이용해서 받아온 name을 new User의 파라미터로 넣어줄 수도 있다.
- required=true가 기본값이기떄문에 꼭 name을 넣어주어야함
- localhost:8080/api/user/param 만 입력했을때
- 400 오류가 발생
- http://localhost:8080/api/user/param?name=테스트
➡️@RequestParam에서 요구하는 name을 넣어주면 그 값으로 실행이 제대로 될 것
@RequestParam(required = false)
@GetMapping("/api/greeting")
public Map<String, String> greet(@RequestParam(name = "message", required = false, defaultValue = "hello")String message){
Map<String, String> res = new HashMap<>();
res.put("message", message);
res.put("key", "[key]에 따른 VALUE!");
res.put("Cookie", "오레오!");
return res;
}
- @RequestParam의 required=false를 두면 message를 꼭 받아오지 않아도 된다는 설정
- required=false
값을 반드시 넣을 필요는 없지만 넣지 않았을 경우에는 defaultValue = “hello”를 출력하도록 설정할 수 있음
RESTful 서비스
- 웹 서비스의 한 형태로 REST(Representational State Transfter) 아키텍처 스타일을 따름
- 자원 기반으로 모든 콘텐츠를 표현함, 자원은 URI(Uniform Resource Identifier)로 식별됨
ex. 사용자 정보를 다루는 서비스에서 각 사용자는 고유한 URI 에 의해 참조가능 - 무상태 (stateless) : 각 요청은 독립적이고 서버는 클라이언트의 상태정보를 저장하지 않음.
- 연결성 : 자원은 하이퍼링크를 통해 서로 연결될 수 있음
- 표준 메소드 사용 - GET, POST, PUT, DELETE 등
- 다양한 표현 : JSON, XML 등 여러 형태의 데이터 포맷을 지원하여 클라이언트의 요구에 맞춰 데이터를 제공 가능
JSON / XML 데이터 처리
- List<User> getUsers() {... return users }
➡️리스트를 반환했지만 JSON 데이터로 보여주는 것을 확인 가능 - 이는 Jackson 라이브러리가 SpringMVC를 통해
JSON 데이터를 자바 객체로 직렬화하거나 역직렬화하고 있는 것이다.
즉 @RestController나 @ResponseBody 어노테이션이 붙은 메소드에서 자동으로 처리되고 있는 것 - 클라이언트 → 메소드에 JSON 형식 데이터 POST 요청으로 전송
- 메소드 → 클라이언트로 (메소드가 자바객체를 반환하면 이 객체는 JSON으로 자동변환되어 클라이언트에게 응답)
XML 사용
의존성 추가
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml'
@GetMapping(value = "/api/user", produces = "application/xml")
public User getUser(){
return new User("Isak", "010-1111-1111", "타인위어");
}
- value와 produces를 파라미터로 넣어서 XML로 처리되도록 구현 가능
다시 JSON으로 변경
@GetMapping(value = "/api/user", produces = "application/json")
public User getUser(){
return new User("Isak", "010-1111-1111", "타인위어");
}
- application/xml → application/json으로 변경
ResponseEntity 클래스
- Spring MVC에서 HTTP 요청에 대한 응답을 구성할때 사용
- HTTP 상태코드, 헤더, 응답본문 등 "부수적인 다른 정보들도 HTTP 응답 처리 시 같이" 보내게 됨
(=응답을 보낼 시 name=”Kaide” phoneNumber=”010-1111-1111”… )처럼 필요정보만 보내는게 아닌
다른 부가정보도 같이 보내는 객체 - 장점 : 개발자는 RESTFul API를 보다 세밀하게 조정 가능
body 추가
@GetMapping("/example/entity")
public ResponseEntity<String> getResEntity(){
return ResponseEntity.status(HttpStatus.OK).body("[ResponseEntity]를 반환합니다!");
}
- localhost:8080/api/example/entity 접속 시
➡️출력 : [ResponseEntity]를 반환합니다!
header 추가
@GetMapping("/example/entity")
public ResponseEntity<String> getResEntity(){
return ResponseEntity.status(HttpStatus.OK).header("Custom-Header", "Kaide").body("[Body] 부분입니다.");
}
- body앞에 header를 추가할 수도 있다.
- 명령프롬프트 창에서 curl 명령어를 통해 해당 URL의 정보를 검색해볼 수 있음
- Custom-Header : Kaide
만들어놨던 Header가 출력됨을 확인 가능
curl 명령어
- 인증, 보안 캐싱 등등에 이 curl -i 명령어가 활용될 수 있음
- curl -X POST http://localhost:8080/api/example
POST 메소드 전송방식으로 바꿀 수 있다.
❓curl -i, curl -X의 차이점
-i | 응답(Response) 헤더를 출력 | curl -i <http://localhost:8080/api/example/entity> |
-X | 요청(Request) 방식(메서드)을 지정 | curl -X POST <http://localhost:8080/api/example/entity> |
- i : 서버의 응답 헤더를 확인하는 옵션
- X : HTTP 메서드(ex. GET, POST, PUT, DELETE 등)를 직접 지정할 때 사용
curl <http://localhost:8080/api/example/entity>
curl -X GET <http://localhost:8080/api/example/entity>
- 이 둘은 똑같은 동작을 수행
@PathVariable 처리
@GetMapping("/user/{id}")
public ResponseEntity<User> getUserById(@PathVariable("id")Long id){
User user = new User(id, "Tyler", "010-1111-1111", "Cheshire");
if(user == null){
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(user);
}
- @PathVariable 어노테이션을 통해 URI로부터 가져와지는 {id}값을 받아들여서
- 새로운 유저를 생성할때 id를 넣어 만들게끔 구현하고
입력되는 값이 없어서 null이게되면 notFound().build() 를 발생시키고
유효한 유저면 ok()에 user를 넣어 반환하도록 함
- localhost:8080/api/user/1
id = 1번 값이 매핑되어 새로운 유저 생성시 그 id값을 가지고 생성되는 것을 확인 가능
Rest API 와 데이터베이스 연결
- 패키지를 나눈 후 (domain-controller-service-repository)
- DB의 기존 jpa_user 테이블을 연결
domain에 User 엔티티 생성
@Entity
@Getter@Setter@NoArgsConstructor
@Table(name = "jpa_user")
public class User {
@Id@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
private String name;
public User(String email, String name) {
this.email = email;
this.name = name;
}
}
repository 생성
public interface UserRepository extends JpaRepository<User, Long> { ... }
🚀service에 UserService 클래스 - CRUD 메소드 작성
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
// 사용자 저장 (CREATE)
public User saveUser(String email, String name){
User user = new User(email, name);
return userRepository.save(user);
}
// 모든 사용자 조회 (SELECT - READ)
public List<User> getAllUsers(){
return userRepository.findAll();
}
// 특정 사용자 조회 (SELECT - READ)
public User getUserById(Long id){
return userRepository.findById(id).get();
}
// JPA가 자동으로 만들어주는 findById()를 이용해서 조회해볼 수도 있음
public User findById(Long id){
return userRepository.findById(id).orElseThrow();
}
// 사용자 삭제 (DELETE)
public void deleteUser(Long id){
userRepository.deleteById(id);
}
}
🚀 UserController에서 실제 웹과의 요청 동작 메소드 구현
@RestController
@RequestMapping("/api/jpa/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
// 사용자 등록 (POST방식)
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user){
User savedUser = userService.saveUser(user.getEmail(), user.getName());
return ResponseEntity.status(HttpStatus.CREATED).body(savedUser);
}
// 모든 사용자 조회 (GET방식)
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
return ResponseEntity.ok(userService.getAllUsers());
}
// 특정 사용자 조회 (SELECT - READ)
public User getUserById(Long id){
return userRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."));
}
// 사용자 삭제 (DELETE)
@Transactional
public void deleteUser(Long id){
if(!userRepository.existsById(id)){
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "삭제할 사용자가 업습니다.");
}
userRepository.deleteById(id);
}
}
- 존재하지 않는 사용자일 경우 orElseThrow()로 처리할 수 있도록 구현
- 사용자 삭제부분에서도 existsById()메소드를 통해 삭제할 사용자가 있는지 없는지를 검사함
- http://localhost:8080/api/jpa/users 접속
- 기존 jpa_user 테이블에 저장되어있던 User데이터들이 출력되고 있음
🚀실습 - Rest API - 사용자 등록 CREATE
- 명령프롬프트 창
curl -v -X POST -H "Content-Type: application/json" --data-raw "{\"email\":\"test@example.com\",\"name\":\"홍길동\"}" http://localhost:8080/api/jpa/users 입력
- DB에 잘 저장됨
🚀실습 - Rest API - 모든 사용자 조회 READ
- curl -X GET http://localhost:8080/api/jpa/users
모든 사용자가 조회되는것을 확인 가능
🚀실습 - Rest API - 특정 사용자 조회 READ
- curl -X GET http://localhost:8080/api/jpa/users/{조회할ID}
[Spring Data JPA] RESTful API 에서 "특정 사용자 조회" 오류 해결
상황⚠️"특정 사용자 조회" 와 "특정 사용자 삭제" 명령어 처리 시 오류 발생원인 가능성- 각 메소드에 orElseThrow() 와 같은 예외 처리를 하지 않은 것- 삭제 메소드에 존재하지 않는 사용자인 경
lefton.tistory.com
✅해결 후 실행결과
🚀실습 - Rest API - 특정 사용자 삭제 DELETE
- curl -X DELETE http://localhost:8080/api/jpa/users/{삭제할ID}
[Spring Data JPA] RESTful API 에서 "특정 사용자 조회" 오류 해결
상황⚠️"특정 사용자 조회" 와 "특정 사용자 삭제" 명령어 처리 시 오류 발생원인 가능성- 각 메소드에 orElseThrow() 와 같은 예외 처리를 하지 않은 것- 삭제 메소드에 존재하지 않는 사용자인 경
lefton.tistory.com
- id=15에 해당하는 "홍길동"이 삭제된 것을 확인 가능
🚀회고 결과 :
이번 회고에서는 Rest API를 @RestController로 구현해보는 실습을 해보고
GET, POST 방식에 대해서 좀 더 알아볼 수 있었다.
유저를 저장하는 CREATE방식에서 curl 명령어를 사용하는 것이 어려웠지만 조회, 삭제에서 발생하는
오류를 해결하기 위한 과정이 뜻깊었던 회고였다.
- Rest API에 대한 이해
- @RestController로 URI 매핑 구성
느낀 점 :
오류가 발생했던 것을 해결하는 과정에서 오류 발생 원인을 분석해볼 수 있었다.
별도의 회고로 작성하면서 다양한 엔티티들과 관계된 엔티티의 toString() 사용, curl로 데이터 조회 시 @PathVariable 설정 등을 해결할 수 있었다.
향후 계획 :
- @PathVariable 동작 흐름 이해
- 다른 @RestController 설계
- 다른 테이블을 이용해 아키텍처 구성해보기
'Recording > 멋쟁이사자처럼 BE 13기' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_51일차_"Criteria + hr DB" (0) | 2025.02.20 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_50일차_"Spring Data JPA" (0) | 2025.02.19 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_49일차_"JPA 상속 관계 매핑" (1) | 2025.02.18 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_48일차_"JPA 관계형 테이블" (1) | 2025.02.14 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_47일차_"JPA 엔티티 매핑" (0) | 2025.02.13 |