목표 : 코드리뷰 받은 내용을 정리하고 JPA에 대해서 배운 내용과 더 공부한 내용을 정리한다.
1. 페이지 응답을 담당하는 핸들러와 클라이언트의 요청을 받는 핸들러를 분리하자
처음에는 한 엔티티에 대한 컨트롤러가 하나여야한다고 생각했어서 어떤 것을 고쳐야하는지 잘 몰랐었는데, 팀원분들께 물어보니까 컨트롤러를 2개 만들어도 된다고 하셨다. 그래서 @RestController을 쓰는 컨트롤러, @Controller을 쓰는 컨트롤러 이렇게 2가지를 만들었다.
@Controller
@RequestMapping("/menus/view")
public class MenuController {
private final MenuService menuService;
public MenuController(MenuService menuService) {
this.menuService = menuService;
}
public String returnView(
String errorMsg,
Model model) {
if (errorMsg != null) {
model.addAttribute("errors", errorMsg);
model.addAttribute("menus", menuService.findall());
return "Menu";
}
model.addAttribute("menus", menuService.findall());
return "redirect:/menu";
여기는 view를 반환한다. 위와 같이 모델에 값을 넣는 작업도 필요 없이 빈 깡통뷰만 리턴해도 된다고 하셨다.
@RestController
@RequestMapping("/menus")
public class MenuRestController {
private final MenuService menuService;
public MenuRestController(MenuService menuService) {
this.menuService = menuService;
}
@PostMapping
public ResponseEntity<Object> save(
@ModelAttribute @Valid MenuRequest request,
BindingResult result
) {
if(result.hasErrors()){
return ResponseEntity.badRequest().body(result.getFieldError().getDefaultMessage());
}
else{
MenuResponse menuResponse = menuService.save(request);
return ResponseEntity.ok().body(menuResponse);
}
}
여기는 ResponseEntity를 반환한다.
2. 계층별 유지보수 범위를 전파시키고 싶지 않기 위해서 요청을 전달하는 객체와 비지니스 로직을 수행할 객체를 분리하자
엔티티와 Response, Request 이렇게 세개의 요소를 만들었다.
Response와 Requset는 recode를 사용하였다.
3. @ControllerAdvice @RestControllerAdvice 의 차이
ControllerAdvice는 주로 view를 반환할 때 쓰이고, RestControllerAdvice은 주로 JSON형태로 반환할 때 쓰인다고 합니다.
제 코드에서 RestControllerAdvice를 썼음에도 오류가 나지 않았던 이유를 찾아봤는데 ResponseEntity로 반환하는 과정에서 자동으로 JSON으로 변환된다고 하였습니다!
하지만 제 코드의 가독성을 위해서는 RestControllerAdvice를 사용하는 것이 좋다고 생각되어 이 부분은 수정하였습니다!
4.MethodArgumentNotValidException 외에도 여러가지 예외가 다양하게 발생할 수 있는데,
그것들은 모두 어떻게 처리해줄 수 있을까요?
package gift.exception;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class EtcExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String,String>> handleEtcExceptions(
Exception exception
){
Map<String,String> errors = new HashMap<>();
errors.put("errerEtc",exception.getMessage());
return ResponseEntity.badRequest().body(errors);
}
}
위와 같이 ExceptionHandler(Exception.class)를 사용해서 설정한 에러 이외의 오류를 처리하였다.
5. 필수값 여부로 NOT NULL 설정을 추가
null이면 안되는 필드에도 위 조건을 추가하지 않았었다. 앞으로는 더 신경써야겠다.
6. JWT토큰은 헤더에
이건 내가 PR에 여쭤본 부분이다.
구현 방식은 자유지만, 보편적으로 사용되는 방식들이 있는데, 이걸 통신규약이라고 말한다.
HTTPS 를 통해 전송되는 헤더는 암호화되어 전송된다. Authorization 헤더를 중간에 가로채더라도 볼 수 없기 때문에 통신규약에 따르면 보안상 이점을 더욱 누리기 위해 header 에 담는 것을 권장한다.
7. 축약어 사용은 지양하자
나는 passwd라는 축약어를 사용했는데 이건 가독성에 좋지 않다고 한다. 그래서 모두 password로 바꿨다.
8. Controller은 클라이언트에게 정보를 전달하는 기능만 집중하도록 하자
나는 비밀번호 일치 여부를 Controller단에서 확인했는데 이러면 controller이 역할에 집중할 수 없다고 한다. 그래서 이 부분을 service가 수행하는 것으로 바꾸었다.
9. 접근제한자를 명시하자
접근제한자를 빼먹었다. 앞으로 리뷰요청하기 전에 이 부분 확인해야겠다.
10. 파일의 끝에 개행문자 넣어주기

나는 위와 같이 파일의 끝에 개행문자를 추가하지 않았었다. 그런데 이렇게 하면 파일이 끝났음을 알릴 수 없어 EOF를 할 수 없다고 한다. 그래서 나는 인텔리제이에서 위 기능을 자동으로 수행해주는 부분을 설정하였다. 이 기능을 설정해주고 저장을 하면 자동으로 반영된다.
멘토님이 블로그 링크를 주셨다!
https://hyeon9mak.github.io/github-no-newline-at-a-end-of-file/
GitHub 파일 끝에 개행이 필요한 이유 (no newline at a end of file)
GitHub 에 Pull Request 를 올리면 가끔 아래와 같은 금지 마크를 발견할 때가 있다.
hyeon9mak.github.io
11. url 이름과 메서드 이름을 동일하게 하자
@PostMapping("/register")
public ResponseEntity<String> join(
@RequestParam("id") String id,
@RequestParam("password") String password
) {
MemberRequest memberRequest = new MemberRequest(id,password);
return memberService.join(memberRequest);
}
처음에는 위와 같이 구현하였는데, 이런 사소한 포인트로 동료 개발자들의 생산성에 차이가 생긴다고 한다.
바로 수정했다
12. 경로명은 복수형으로 하자
@RestController
@RequestMapping("/menus")
public class MenuRestController {
private final MenuService menuService;
public MenuRestControl
URL 경로를 볼 때 만약 "/menus/{id}' 이런 식으로 받으면 메뉴들 중 해당 아이디에 해당하는 것을 ..해라 이렇게 해석한다고 한다
이를 용이하게 하기 위해서는 복수형을 쓰는 것이 좋다.
13. @ModelAttribute와 @RequsetBody의 차이점은?
ModelAttribute는 눈에 보이는 빈 생성자가 있다면 무조건 setter가 있어야 한다.
이때문에 내가 Recode를 써도 오류가 나지 않았던 것이다.
반면 RequestBody는 setter가 없어도 된다.
14. 옳지 않은 부분이 생기면 null반환등의 기능을 넣지 않고, 바로 Exception을 발생시킨다.
나는 원래 null을 반환하여 이걸 받으면 예외처리를 하도록 하였는데, 그냥 그 자리에서 Exception을 발생시키고 Exception핸들러가 이를 처리하도록 하는 것이 좋다.
15.ResponseEntity를 다루는 것은 Controller에서!
나는 완성된 객체를 반환해야한다는 생각에 ResponseEntity도 Service에서 다루도록 하였는데, 이 부분은 Controller의 역할이라고 한다.
16. 불필요한 import 다 지우자
import 남겨두는게 괜찮다고 생각했는데 다른 개발자가 볼 때 혼란스러울 수 있다고 한다.
패키지를 클릭하고 Control + option + o 하면 된다고 하셨다.
17. DTO -> ENTITY 매핑하는 부분 엔티티나 Controller 부분에 Static로 선언하는 방식도 있다.
이 부분은 필드값이 있는 것이 아니고, 단순히 구현만 하므로 static으로 선언하여 사용해도 된다고 하셨다
18. 에러메세지를 바로 반환하지 않고 커스텀메세지를 쓰자
나는 에러를 처리할 때 에러메세지를 있는 그대로 클라이언트에게 전달했는데, 이렇게 하면 보안상의 이유로 좋지 않아서
"예상치 못한 오류가 발생하였습니다. 다시 시도해주세요"와 같은 커스텀 메세지를 사용하는게 좋다고 하셨다
19.Optional을 쓰자
Optional을 쓰면 null도 처리할 수 있고, 빈 값이 왔을 때 바로 exception을 날릴수도 있다고 한다.
20. PatchMapping이란?
PatchMapping이란 리소스의 일부를 수정하는 것이다.
put과는 달리 리소스의 전체를 교체하지 않는다.
만약 name만 변경하고싶다면
@PatchMapping("api/name/{id}")
이런식으로 설정하고 해당 메서드에서 name만 수정하고 이를 Repository에 보내면 된다.
JDBC는 쿼리문을 써주고, Repository를 직접 써줘야했지만, JPA는 이를 훨씬 간편하게 구현할 수 있게 해준다.
SQL을 직접 다룰 때의 문제점
새로운 필드가 추가되면 관련된 SQL을 다 수정해야한다. 또한 개발자들이 엔티티를 신뢰하고 사용할 수 없다.
스키마 생성하는 방법
application.properties에
spring.jpa.hibernate.ddl-auto=create
이를 추가한다.
create: 기존 테이블 삭제 후 다시 생성 (DROP + CREATE)
create-drop: create와 같으나 종료시점에 테이블 DROP
update: 변경된 부분만 반영 (운영 DB에 사용하면 안됌)
validate: entity와 table이 정상 매핑되었는지만 확인
none: 사용하지 않음
@Entity
public class Menu {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Entity
엔티티 클래스임을 지정하며 테이블과 매핑된다.
@Table
엔티티가 매핑될 테이블을 지정하고 생략 시 엔티티 클래스 이름과 같은 테이블로 매핑된다.
@Id
id 필드임을 나타낸다.
@GeneratedValue
pk의 생성 규칙을 나타낸다. 위와 같이 하면 id Long일 때 자동으로 id값이 생성된다.
@Column
컬럼의 이름을 지정된 필드나 속성을 테이블의 컬럼에 매핑한다.
컬럼 이름을 지정하지 않으면 자동으로 필드의 이름으로 지정된다. 또한 nullable = false라고 선언하면 NOT NULL 속성이 추가된다
SpringData JPA
SpringData JPA를 쓰기 전에는 Repository에 관련 메서드를 직접 구현을 해야했다. 하지만 JPA를 쓰면 이러한 것들을 상속을 통해서 구현이 가능하다
JPARepository
JPA를 사용하여 데이터베이스를 조작하기 위한 메서드들을 제공한다.
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u WHERE u.age >= :age")
List<User> findByAgeGreaterThanEqual(@Param("age") Integer age);
}
이렇게 커스텀할수도 있고,
count, delete, deleteAll, deleteAllById, deleteById, existsById, findById, save 등의 기본적인 메서드를 제공해서 이걸 쓰면 된다!
findAll, findAllById, saveAll : List로 받는다.
영속성 컨텍스트
엔티티를 영구 저장하는 환경
persist() 메소드는 엔티티 매니저를 사용해서 회원 엔티티를 영속성 컨텍스트에 저장한다.
영속성 컨텍스트는 엔티티 매니저를 생성할 때 하나 만들어지고, 이를 통해서 영속성 컨텍스트에 접근할 수 있고, 이를 관리할 수 있다.
엔티티에는 4가지 상태가 존재한다.
비영속(New) : 엔티티 객체를 생성하고 아직 저장 안함
영속(Managed) : 엔티티 매니저를 통해서 엔티티를 영속성 컨텍스트에 저장
준영속(Detached) : 영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 영속성 컨텍스트가 관리하지 않음
삭제(Removed) : 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제

생명주기는 이러하다
영속성 컨텍스트의 특징
영속성 컨텍스트는 엔티티를 식별자 값으로 구분한다. 따라서 영속상태는 식별자 값이 반드시 있어야한다.
JPA는 보통 트랜젝션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 데이터베이스에 반영하는데 이를 플러시(flush)라고 한다.
영속성 컨텍스트가 엔티티를 관리하면
-1차 캐시
-동일성 보장
-트랜젝션을 지원하는 쓰기 지연
-변경 감지
-지연 로딩
member 엔티티를 생성하고, em.persist(member)을 하면

이와 같은 상태가 된다.
em.find를 통해서 엔티티를 조회할 때에는
Member member = em.find(Member.class,"member1");
위와 같이 엔티티 클래스 타입과, 엔티티 식별자값을 파라미터로 넣으면 조회가 되고, 먼저 1차 캐시에 해당 값이 존재하는지 찾고, 만약 없으면 데이터베이스에서 조회한다.
만약 em.find()를 호출했는데 엔티티가 1차 캐시에 없으면 엔티티 매니저는 데이터베이스를 조회해서 엔티티를 생성한다. 그리고 이를 1차 캐시에 저장한 후 영속상태의 엔티티를 반환한다.

영속성 엔티티의 동일성 보장
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
여기서 a == b 하면 true가 반환된다.
동일성 : 실제 인스턴스가 같다.
동등성 : 실제 인스턴스는 다를 수 있지만 값이 같다.
엔티티 등록
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경 시 트랜잭션을 시작해야 한다.
transaction.begin(); //[트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
//커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); //[트랜잭션] 커밋
위와 같이 트랜젝션을 사용하면 엔티티 매니저는 트랜젝션을 커밋하기 직전까지 데이터베이스에 엔티티를 저장하지 않고 내부 쿼리 저장소에 INSERT SQL을 차곡차곡 모아둔다. 그리고 트랜젝션을 커밋할 떄 이 쿼리들을 데이터베이스에 보내는데 이것을 '트랜젝션을 지원하는 쓰기지연'이라고 한다.

우선 A를 영속화한다.

그 다음 B를 영속화한다.

이렇게 트랜젝션을 커밋하면 엔티티매니저는 영속성 컨텍스트를 flash한다.
이렇게 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하고, 실제 데이터베이스에 트랜잭션을 커밋한다.
참조
[JPA] JPARepository에 대해 알아보자(+사용법, Method)
안녕하세요. 쭈피셜입니다!9기 여러분 모두 개발은 잘하고 계신가요?Spring 수업도 끝이 나고 이제 대망의 프론트수업만 남아 있습니다.점점 종강이 가까워지고 있습니다. 종강이 가까워진다는
velog.io
https://m.yes24.com/Goods/Detail/19040233
자바 ORM 표준 JPA 프로그래밍 - 예스24
자바 ORM 표준 JPA는 SQL 작성 없이 객체를 데이터베이스에 직접 저장할 수 있게 도와주고, 객체와 관계형 데이터베이스의 차이도 중간에서 해결해준다. 이 책은 JPA 기초 이론과 핵심 원리, 그리고
m.yes24.com
소감
JDBC를 쓰다가 JPA를 써보니까 너무 편해서 놀랐지만, 팀원들과 이야기해보니까 여러가지 문제점이 있는 것 같다. JPA를 더 공부해서 안정적인 프로그램을 짤 수 있도록 해야겠다.
그리고 코드리뷰를 받아보니까 사소한 것들로도 동료 개발자들의 생산성에 영향을 미칠 수 있다는 것을 알게되었다. 협업이 중요한 만큼 앞으로 더욱 신경써야겠다
'활동정리 > 모각코' 카테고리의 다른 글
2024 하계 모각코 4회차 (0) | 2024.07.20 |
---|---|
2024 하계 모각코 3회차 (1) | 2024.07.16 |
2024 하계 모각코 1회차 (0) | 2024.07.13 |
2024 동계 모각코 4회차 활동정리 (0) | 2024.01.22 |
2024 동계 모각코 3회차 활동정리 (0) | 2024.01.22 |