https://gmlwjd9405.github.io/2019/10/28/intellij-jpa-erd.html

 

[IntelliJ] intellij에서 JPA Entitiy 기반의 ERD 그리기 - Heee's Development Blog

Step by step goes a long way.

gmlwjd9405.github.io

 

|  개요

- @RequestParam, @RequestBody

- @RequestParam은 URI를 통해서 넘겨 받는 값을 말하고,

- @RequestBody는 Http body에 데이터를 묶어서 받는 값을 말한다.

- 일반적으로 RequestParameter는 ?key=value와 같이 단일 데이터를 넘겨 받고,

  RequestBody는 x-www-form-urlencoded나 Json과 같이 특정 데이터 포맷으로 묶여서 담겨지는 경우가 많다.

- Post 방식으로 넘겨 받는 URI 모습

  *body 값으로 다양한 데이터 포맷이 존재하는 걸 볼 수 있다. 

|  실험

- POST 방식으로 Body에 담겨 넘어오는 데이터의 타입을 크게 두 가지로 분류해보았다. (물론 데이터 타입은 더 다양하다)

  1) x-www-form-urlencoded (form 디폴트 타입)

  2) json 타입

- Annotation, @RequestParam, @RequestBody에 따라

  두 가지 타입의 데이터를 어떻게 읽어오는지 아래의 메소드들을 사용해서 확인해보았다.

// 아래 각 케이스에 대하여 x-www-form / json 타입의 요청을 전달한다.

// case 1 : 단일 데이터 + @RequestParam
@PostMapping("/register")
public void register(@RequestParam String id){
    System.out.println(id); 
}

// case 2 : 단일 데이터 + @RequestBody
@PostMapping("/register2")
public void register2(@RequestBody String id){
    System.out.println(id); 
}

// case 3 : 객체 데이터 + @RequestParam
@PostMapping("/register3")
public void register3(@RequestParam UserRegister userRegister){
    System.out.println(userRegister); //
}

// case 4 : 객체 데이터 + @RequestBody
@PostMapping("/register4")
public void register4(@RequestBody UserRegister userRegister){
    System.out.println(userRegister); //
}

----- ++ 추가적으로 Annotion 미사용시 케이스도 확인

// case 5 : 단일 데이터 + Annotation 없음
@PostMapping("/register5")
public void register5(String id){
    System.out.println(id); //
}

// case 6 : 객체 데이터 + Annotation 없음
@PostMapping("/register6")
public void register6(UserRegister userRegister){
    System.out.println(userRegister); //
}

 

|  결과

  x-www-form-urlencoded json
단일 객체 단일 객체
요청 ) id=test1234 id=test1234&pwd=1234 {
    "id":"test1234"
}
{
     "id": "test1234",
     "pwd":"1234"
@RequestParam test1234 400 에러** 400 에러* 400에러****
@RequestBody id=test1234 415 에러*** {
    "id":"test1234"
}
UserRegister(id=test1234, pwd=null)
Annotation 없음 test1234 com.example.demo.dto.UserRegister@498727e7 null com.example.demo.dto.UserRegister@498727e7

*400 : Required request parameter 'id' for method parameter type String is not present

**400 : Required request parameter 'userRegister' for method parameter type UserRegister is not present

***415 : Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported

****400 : Required request parameter 'userRegister' for method parameter type UserRegister is not present

 

|  정리

- @RequestParam은 x-www-form-urlencoded 타입의 단일 변수 값을 읽어올 수 있다.

- @RequestBody는 json 타입의 단일 또는 객체 값을 읽어올 수 있다.

  x-www-form-urlencoded json
단일 객체 단일 객체
사용방식
@RequestParam 또는
Annotaion 미사
Annotation 미사용 @RequestBody @RequestBody 또는
Annotaion 미사

 

|  상황

- repository.save()를 하면 Entity타입의 객체가 반환되어야 하는데 아래와 같이 null이 반환됐다.

/*Q14*/
// 트러블 슈팅 : 어? 뭐지? 데이터가 저장은 됐는데, Response값이 안온다.
// -- 원인 ) Entity 인스턴스 비교(equals)를 위해 equals, hashcode 필요
@PostMapping("/api/notice4")
public Notice addNotice4(@RequestBody NoticeRegister noticeRegister){
    return noticeRepository.save(
        Notice.builder()
                .title(noticeRegister.getTitle())
                .contents(noticeRegister.getContents())
                .build()
    );
}

>> Response

{}

 

|  해결방법

- Entity 객체에 @EqualsAndHashCode가 추가되면 된다.

- 나는 그냥 @Data를 넣어주었다.

 

[참고]

https://stackoverflow.com/questions/68429396/jparepository-save-methods-returns-null

 

|  User IP와 User Agent 정보란?

- 클라이언트가 HTTP를 통해 어떤 요청을 보내면 HTTP header에 사용자 IP주소와 기기정보(Agent)가 담기게 된다.

- User IP 주소는, 다양한 종류의 proxy를 고려하여 각 header를 전부 확인하는 것이 필요하다.

- 만약 IPv4 형식으로만 IP주소를 얻길 원한다면 [Run]-[Configuration] Arguments VM에 설정을 걸어줄 수 있다.

-Djava.net.preferIPv4Stack=true

- 사이트 정책 : 로그인 시 히스토리 내역 저장한 후 메인화면으로 이동한다.

- 위 정책에 따라

 1) 히스토리를 저장할 Entity를 만들고,

 2) 스프링 시큐러티를 이용해 로그인 성공시 사용자 IP주소와 Agent정보를 DB에 저장한다.

- 스프링 시큐러티 레퍼런스 : https://atin.tistory.com/585

 

1. Entity

@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class LoginHistory {
    @Id
    @GeneratedValue
    private Long id;

    private String userId;
    private String userIp;
    private String userAgent;
    private LocalDateTime lastLoginDate;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

}

 

2. WebUtils에 사용자 IP와 Agent 정보를 가져오는 메소드를 만들었다.

package com.example.fastlms.util;

import javax.servlet.http.HttpServletRequest;

public class WebUtils {

    public static String getUserAgent(HttpServletRequest request) {
        return request.getHeader("User-Agent");
    }

    public static String getClientIp(HttpServletRequest request) {

        String ip = request.getHeader("X-Forwarded-For");

        if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
            ip = request.getHeader("x-real-ip");
        }
        if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
            ip = request.getHeader("x-original-forwarded-for");
        }
        if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
            ip = request.getHeader("HTTP_X_FORWARDED");
        }
        if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
            ip = request.getHeader("HTTP_X_CLUSTER_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
            ip = request.getHeader("HTTP_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
            ip = request.getHeader("HTTP_FORWARDED");
        }
        if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
            ip = request.getHeader("HTTP_VIA");
        }
        if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
            ip = request.getHeader("REMOTE_ADDR");
        }
        if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
            ip = request.getRemoteAddr();
        }

        return ip;
    }
}

 

3. 스프링 시큐러티를 통해 userId, userIP, userAgent를 LoginHistory(DB)에 저장

(1) SecurityConfig에서 Config(HttpSecurity http)의 defaultSuccessUrl() 사용 

- 로그인 Success Handler를 만드는 방법도 있다고 하는데, 메소드 사용이 너무 복잡해서, 

  대신 httpSecurity API 메소드의 defaultSuccessUrl()을 사용했다.

- defaultSuccessUrl()을 통해 로그인 성공 시 특정 경로로 이동하여 로그인 히스토리를 저장할 것이다.

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final MemberService memberService;

    @Bean
    UserAuthenticationFailureHandler getFailureHandler() {
        return new UserAuthenticationFailureHandler();
    }

    @Bean
    PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * Http Security
     * 모두 접근 가능 : 홈, 회원 가입, 메일 인증
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable();

        http.headers().frameOptions().sameOrigin();

        http.authorizeRequests()
                .antMatchers(
                        "/",
                        "/member/register",
                        "/member/email-auth",
                        "/member/find/password",
                        "/member/reset/password"
                )
                .permitAll();

        http.authorizeRequests()
                .antMatchers("/admin/**")
                .hasAuthority("ROLE_ADMIN");

        http.formLogin()
                .loginPage("/member/login")
                //.successForwardUrl("/member/login-success-handler")  // 로그인이 성공한 후 보내는 URL
                //.successHandler(getSuccessHandler())                 // 로그인 핸들링 URL
                .defaultSuccessUrl("/member/login-success")            // 로그인이 성공한 후 보내는 디폴트 URL
                .failureHandler(getFailureHandler())
                .permitAll();

        http.logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/member/logout"))
                .logoutSuccessUrl("/")
                .invalidateHttpSession(true);

        http.exceptionHandling()
            .accessDeniedPage("/error/denied");

        super.configure(http);
    }

}

(2) 컨트롤러에서 사용자 정보를 가져온다.

- Principal로 userId를 가져오고,

- WebUtils를 통해 Request Header의 IP와 Agent 정보를 가져온다.

@Controller
@RequiredArgsConstructor
@Slf4j
public class MainController {

    private final LoginHistoryService loginHistoryService;

    @RequestMapping(value = "/")
    public String index() {

        return "index";
    }

    @RequestMapping(value = "/member/login-success")
    public String saveLoginHistory(HttpServletRequest request, Principal principal){

        String userId = principal.getName();
        String userIp = WebUtils.getClientIp(request);
        String userAgent = WebUtils.getUserAgent(request);

        loginHistoryService.saveLoginHistory(userId, userIp, userAgent);

        return "index";
    }

}

(3) 로그인 서비스를 통해 히스토리를 DB에 저장한다. 

@Service
@RequiredArgsConstructor
public class LoginHistoryImpl implements LoginHistoryService {

    private final LoginHistoryRepository loginHistoryRepository;

    @Override
    public void saveLoginHistory(String userId, String userIp, String userAgent) {

        loginHistoryRepository.save(
                LoginHistory.builder()
                        .userId(userId)
                        .userIp(userIp)
                        .userAgent(userAgent)
                        .lastLoginDate(LocalDateTime.now())
                        .build()
        );

    }
}

 

 

 

[ 참고 및 출처 ]

https://recordsoflife.tistory.com/248

https://galid1.tistory.com/698

https://all-record.tistory.com/168

https://atoz-develop.tistory.com/entry/Spring-Security-Web%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-IP-%EC%A0%91%EA%B7%BC-%EC%A0%9C%EC%96%B4-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0

|  JPA

- JPA란, 자바에서 사용하는 ORM 기술 표준을 말한다.

* ORM : 객체와 RDBMS를 매핑해주는 기술

- JPA는 인터페이스의 모음으로, Hinernate / EclipseLink / DataNucleus가 이 명세를 구현했다.

 

|  JPA를 왜 쓸까?

1. SQL 중심 개발 -> Object 중심 개발

2. 생산성 : 데이터의 CRUD가 훨씬 쉬워진다.

저장 : jpa.persist(entity)

조회 : jpa.find(pk)

수정 : entity.setName(" ")

삭제 : jpa.remove(entity)

3. 유지보수 : 필드 변경 시 모든 SQL 수정 --> 필드를 하나만 더 추가

4. 패러다임 불일치 해결

rf. 객체와 관계형 DB의 차이

  객체 SQL
상속 객체 상속 관계 Table 슈퍼 타입 - 서브 타입 (Pk - Fk)
연관관계 참조 사용 (ex. member.getTeam()) 외래 키를 사용 (양방향 조인이 가능)
객체 그래프 탐색 상속 관계에서 부모 타입 사용 가능 서로 pk,fk를 통해 조인된 테이블끼리만 가능
비교하기 같은 참조값을 가진 객체는 서로 == 같다고 표현 동일한 트랜잭션에서 조회한 엔터티여야만 같다고 표현

5. 성능 :

(1) 1차 캐시와 동일성 보장

: JPA를 쓸 경우에 하나의 트랜잭션에서 다음 트랜잭션으로 넘어가기 전에 잠시 동안 캐시를 해주는 기능이 있다.

(2) 트랜잭션을 지원하는 쓰기 지연

insert JDBC BATCH SQL 기능을 통해 commit() 전까지 insert를 모아 한번에 전송한다.
update/delete update, delete로 인한 row 락 시간 최소화 - update, delete 후 바로 commit()

(3) 지연 로딩

* 일반적으로 지연 로딩으로 먼저 코딩 후, 최적화를 위해 자주쓰는 두 객체간 연관관계를 즉시 로딩으로 변경한다.

지연 로딩 객체가 실제 사용될 때에 로딩한다.

-- A 테이블을 조회, B 테이블을 조회 ... 식으로 지연하여 조회
즉시 로딩 Join을 통해 연관 객체를 미리 묶어 조회

-- A join B 를 통해 바로 조회 

6. 데이터 접근 추상화와 벤더 독립성

7. 표준

 

|  역사

- JPA는 Java Persistence API 의 줄임말

- 이클립스 재단에서 가져가면서 Jakarta Persistence API 로 명칭 변경

- 과거에 EJB ORM이 있었다. --> 하이버네이트 (오픈 소스) --> JPA (자바 표준)

 

|  특징

- Annotation을 통해 매핑

- 기본형 타입에 대한 매핑 지원

- 밸류 타입에 대한 매핑 가능

- 클래스 간 연관관계 : 1:1, 1:N, N:1, N:M

- 상속 매핑 지원

 

 

[ 출처 ]

인프런 강의, [ 자바 ORM 표준 JPA 프로그래밍 - 기본편 ] 을 들은 후 정리한 내용입니다.

 

 

 

 

|  개요

- 영속성이란, 프로그램이 종료된 이후에도 한 번 처리한 데이터는 DB에 영구적으로 반영된 상태여야 함을 뜻한다.

- 자바를 통해서 DB와 소통하는 방식은 다양한데,

  그 중에서 Persistence Framework를 사용하면 Object를 통해 SQL문을 만들 수 있다.

- Persistence Framework는 크게 1) SQL Mapper 2) ORM 으로 나뉠 수 있으며, 두번째 방식이 현재 가장 많이 사용되는 방식이다.

 

[ 영속성이란? ]

영속성이란, 데이터를 생성한 프로그램이 종료된 후에도 사라지지 않는 데이터 특성

[ 출처 ] 위키백과

 

[ 자바에서 데이터를 저장하는 방법 ]

- JDBC 프로그래밍 (순수 Java)

- Spring JDBC

- Persistence Framework : Hibernate, MyBatis....
  - 크게 SQL Mapper와 ORM으로 나뉜다.

 

[ Persistence Framework의 종류 ]

구분 내용 프레임워크명
SQL Mapper SQL mapper를 통해 sql <-> object Mybatis, JdbcTemplates(Spring)
ORM ORM을 통해 sql <-> object JPA, Hibernate

 

|  ORM

- Object Relational Mapping : 객체 관계 매핑의 약자

- 객체와 DB의 관계를 매핑해주는 도구를 말한다.

장점 - 직관적인 코딩을 통해 가독성을 높히고, 비즈니스 로직에 집중할 수 있어 생산성이 높아진다.
- 재사용 및 유지보수의 편리성 증가
- DBMS에 대한 종속성이 저하된다.
단점 - 완벽한 ORM만으로만 서비스를 구현하기 어렵다.
- 프로시저가 많은 시스템에서는 ORM의 객체 지향적인 장점을 활용하기 어렵다.

 

|  언어별 ORM 종류

 

|  JPA와 Hibernate, Spring Persistence API

- JPA는 Java Persistece API의 약자로, 말 그대로 인터페이스이자 명세를 의미한다.

- 그리고 JPA를 구현한 것이, Hibernate이다. 

- Spring Data JPA는 Spring 프레임워크에서 사용할 수 있게, JPA를 한단계 추상화시킨 모듈이다. 

출처:https://suhwan.dev/2019/02/24/jpa-vs-hibernate-vs-spring-data-jpa/

 

 

 

[참고 및 출처]

https://gmlwjd9405.github.io/2019/02/01/orm.html

https://hanamon.kr/orm%EC%9D%B4%EB%9E%80-nodejs-lib-sequelize-%EC%86%8C%EA%B0%9C/

https://azderica.github.io/00-db-orm/

https://suhwan.dev/2019/02/24/jpa-vs-hibernate-vs-spring-data-jpa/

|  Validation

- 지난 번 스프링을 처음 배울 당시 아래와 같이 Validation에 대한 개념을 배웠었다.

✅ Validation은 유효성 검증을 말하며, 주로 HTTP Request에서 잘못된 내용을 검증할 때 사용한다.
- 데이터 검증     : 필수 데이터 / 문자열 길이 및 숫자형 데이터 범위 / 이메일 및 신용카드 번호 등 형식 확인
- 비즈니스 검증 : 서비스 정책에 따라 데이터 검증
✅ Validation 방식
1) Java Bean Validation : dto클래스 맴버에 Annotaion(ex. @NotBlank, @Size, @Email...)을 붙이는 방식
2) Spring validator 인터페이스 구현을 통한 validation 
✅ Validation 주의사항 : Validation을 멀리 두면 테스트 및 유지보수성이 떨어지므로 가능한 Annotaion방식을 쓰는 게 좋다.
* 수업에서의 제안 : 1차로 dto에 Annotation방식 사용, 2차로 비즈니스 검증 후 실패 시 Custom Exception throw

- 오늘은 계좌 프로그램의 계좌 생성 기능에 대한 단위 테스트를 하면서 위 데이터를 검증하고, 비즈니스 로직을 검정하는 실습해보았다.

- 이것을 연습 삼아서 동물 병원 차트를 생성하는 걸로 바꿔서 해보았다.

 

1.  데이터 검증하기 - Common Validation Annotations(Java Bean Validation)

  • @NotNull: to say that a field must not be null.
  • @NotEmpty: to say that a list field must not empty.
  • @NotBlank: to say that a string field must not be the empty string (i.e. it must have at least one character).
  • @Min and @Max: to say that a numerical field is only valid when it’s value is above or below a certain value.
  • @Pattern: to say that a string field is only valid when it matches a certain regular expression.
  • @Email: to say that a string field must be a valid email address.

    [출처] https://reflectoring.io/bean-validation-with-spring-boot/

- 수업에서는 HTTP 요청/응답 데이터를 검증할 때, DTO객체에서 Java Bean Validation을 썼다.

- 연습 삼아 동물 병원 차트를 통해 정리해보았다.

- 아래는 동물 병원 차트 도메인 객체

// import 생략

// Lombok getter/setter/생성자 생략
@Entity
@EntityListener(AuditingEntityListener.class)
public class Chart{

    @Id
    @GeneratedValue   // sequence (auto-increment)
    private Long id;
    
    private String chartNo;   // 차트번호 (ex. AA20221211 - 종분류+생성일자)

    @ManyToOne
    private Owner owner;      // 주인
    private Patient patient;  // 환자 기본 정보 (이름, 나이, 성별 등)
    private VaccineRecord vaccineRecord; // 백신 여부
    
    private String memo;      // 증상 메모
    private String diagnose;  // 진단명 
    
    @CreatedDate
    private LocalDateTime createdAt;
    
    @LastModifiedDate
    private LocalDateTime updatedAt;
}

 

[1] DTO : Request/Response Data-binding용 객체에 제약조건 걸기

[ 예시 ] 동물 병원 차트 생성 DTO - Request와 Response 시 Data-binding용
- 요청시 : 주인 id와 동물 id는 반드시 받아 존재하는지 확인한다. / 차트에 메모와 진단이 있을 수도 있고 없을 수도 있다.
- 응답시 : 생성된 차트의 차트번호 반환
// import 생략

public class CreateChart{

   // lombok 게터/세터/생성자 생략
   public static class Request{
    	@NotNull
        @Min(1)
    	private Long ownerId;
        @NotNull
        @Min(1)
        private Long patientId;
        private String memo;
        private String diagonose;
   }
   
   // lombok 게터/세터/생성자 생략
   public static class Response{
        @NotNull
        private String chartNo;
   }
   
   public static Response from(ChartDto chartDto){
        return Response.builder()
        	.ownerId(chartDto.getOwnerId())
        	.patientId(chartDto.getPatientId())
        	.memo(chartDto.getMemo())
        	.diagonose(chartDto.getDiagnose())
        	.build();
   }

}

 

[2] DTO :  도메인에서 필요한 데이터만 넘겨주는 객체 생성

* 지연 로딩을 위해서 필요한 객체이다. (지연 로딩이란? JPA에서 프록시 객체 - 참조값을 가진다 - 로 Entity간 연관관계를 주입할 때, 한 번에 모든 데이터를 올리는 것이 아니라 필요에 따라 지연하여 데이터를 로딩하는 방식)

* 지연 로딩을 하게 되면 사용자의 요청사항에 유연하게 반응할 수 있다.

// import 생략

// lombok 관련 annotation 생략
public class ChartDto {
    private String chartNo;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    
    public static ChartDto fromEntity(Chart chart){
    	return ChartDto.builder()
            .chartNo(chart.getChartNo())
            .createdAt(chart.getCreatedAt())
            .updatedAt(chart.getUpdatedAt())
            .build();
    }
}

 

[3] Controller에서 @Valid로 요청 시 데이터 제약사항 검증하기

[ 예시 ] 동물 병원 차트 생성 컨트롤러 - REST 방식의 컨트롤러

POST http://ip주소:port번호/chart
content-type: application/json

{
        "ownerId": 1,
        "name": "여름이"
}
// import 생략

@RestController
@RequiredArgsConstructor
public class ChartController{
    private final ChartService chartService;
    
    @PostMapping("/chart")
    public CreateChart.Response createChart(@RequestBody @Valid CreateChart.Request request){
    	
        return CreateChart.Response.from(
        	chartService.createChart(
        		request.getOwnerId(),
        		request.getPatientId(),
        	)
        );
        
    }
}

 

2.  비즈니스 로직 검증하기 - Custom Exception

[1] exception : 예외사항에 대한 Custom Exception을 만든다.

(1) 먼저 Enum으로 예외 코드를 만든다.
(2) 해당 Enum을 담은 커스텀 Exception을 만든다.
// import 생략

@Getter
@AllArgsConstructor
public Enum ErrorCode {
    OWNER_NOT_FOUND("주인이 없습니다."),
    PATIENT_NOT_FOUND("환자가 없습니다."),
    CHART_NO_NOT_FOUND("차트번호를 찾을 수 없습니다.")
    
    private String description;
}
// import 생략

// lombok 생략
public class ChartException extends RuntimeException {
    ErrorCode errorCode;
    String errorMessage;
    
    public ChartException(ErrorCode errorCode) {
    	this.errorCode = errorCode;
        this.errorMessage = errorCode.getDescription();
    }
}

 

[2] service : 비즈니스 로직에 따라 서비스 객체 생성하기

[ 예시 ] 동물 병원 차트 생성 서비스 - 비즈니스 로직 검증
- 주인이 있는지 조회하기 
- 동물이 있는지 조회하기
- 동물의 차트가 100개를 넘어가는지 확인 (*validate를 위해 임의로 만든 것)
- 최신 차트 번호 생성
- 차트 저장 후, 차트 정보를 넘긴다.
// import 생략

@Service
@RequiredArgsConstructor
public class ChartService {
    private final ChartRepository chartRepository;
    private final OwnerRepository OwnerRepository;
    private final PatientRepository PatientRepository;
    
    @Transactional
    public ChartDto createChart(Long ownerId, Long patinetId){
    	Onwer owner = ownerRepositroy.findById(ownerId)
        	.orElseThrow(() -> new ChartException(ErrorCode.OWNER_NOT_FOUND));
            
        Patient patient = patientRepository.findById(patientId)
        	.orElseThrow(() -> new ChartException(ErrorCode.PATIENT_NOT_FOUND));
            
        // 차트 100개 넘는지 별도로 validate 처리
        // -- [ctrl]+[alt]+[M]으로 메소드를 extract
        validate(patient);  
        
        String chartNo = chartRepository.findFirstByOrderByIdDesc()
        	.map(chart -> (Integer.parseInt(chart.getChartNo().substring(2) + 1)) + "")
        	.orElseThrow(() -> new ChartException(ErrorCode.CHART_NO_NOT_FOUND));
        
        return ChartDto.fromEntity(
        	chartRepository.save(
            	Chart.builder()
                	//생략
                	.build()
            )
        );        
    }
    
    public void validate (Patient patient) {
    	if (chartRepository.CountByPatient(patient) == 100) {
        	throw new ChartException(ErrorCode.MAX_CHART_COUNT_100);
        }
    }
}

 

[ 참고 및 출처 ]

부트캠프 수업 내용

https://reflectoring.io/bean-validation-with-spring-boot/

|  정리하게 된 계기

- 오늘 수업에서는 계좌 관련 프로그램을 만드는 걸 시작했고,

  계좌 생성 API를 만들며, 사용자 및 계좌에 대한 Entity를 만들었었다.

- 만들다보니, @Entity, @EntityListeners, @id, @Generated의 상위 패키지persistence로 되어 있는게 보였다.

- 더불어, 객체 이벤트 발생 시간을 알려주는 @EntityListeners(AuditingEntityListener.class) 코드의

  AuditingEntityListener.class로 가, 설명을 읽어보니,

  "이건 Entity에 persist 또는 update 이벤트가 발생하는 시간을 알려주는 일회성(stateless) 클래스이다"

  라고 적혀있어, persist가 뭐지? 하는 생각을 하게 됐다.

 

|  JPA에서 말하는 영속성이란

- JPA는 풀어서 Java Persistence API라고 했다.

- 앞서서는 위의 persistence란 자바 코드를 유지하는 것을 말한다고 했는데,

  사실 * persist()란, "영구적으로 DB에 반영하다"를 의미한다.

  그러나 JPA는 정확히 자바를 통해 DB에 영구적으로 반영하는 API라는 의미였다.

- 우리가 JPA를 통해 어떤 테이블, 다른 말로 Entity를 만들고자 하면, 아래와 같은 단계를 거칠 수 있다.

  비영속(new) 영속(managed) 준영속(detached) 삭제(removed)
메소드 new persist() detach() - 특정 entity 분리 remove()
특징 자바 Object 생성 DB에 반영 엔터티 매니저 관리X 삭제
persistence
context에
Entity포함
X O X X

- 영속성 컨텍스트(persistence context)는 실제하진 않고 논리적인 개념으로, 엔터티를 영구 저장하는 환경을 말한다.

- 우리는 자바를 통해 객체를 만들고 이를 DB에 반영할 것을 JPA api에게 요청한다. 

  요청이 DB에 반영이 되는 순간이 곧, persist()되는 순간이다.

출처:https://victorydntmd.tistory.com/207

- 그러면, 다시 Entity를 만드는 자바 코드로 돌아가서 생각을 해보겠다.

package com.example.animal.domain;

import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.time.LocalDateTime;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
@EntityListener(AuditingEntityListener.class)
public class Patient{	
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private String kind;
    private String sex;
   
    @OneToMany
    @JoinColumn(name = "PatientOwner_id")
    private PatientOwner patientOwner;
    
    @CreatedDate
    private LocateDate createdAt;
    @LastModifiedDate
    private LocateDate updatedAt;
}

- 앞에서 만들었던 동물병원 진료 예약 시스템에서, 동물을 Patient로 해서 만들어봤다. 

  간단한 내역만 적어두었는데, Entity에 관한 Annotation import를 보면 persistence가 패키지명으로 들어간 게 보인다.

- 다시 말해 @Entity 표시만 해두고, @Id나 @GeneratedValue로 pk를 설정하면 알아서 DB에 영구 반영시키겠단 말이다.

- 그리고 @EntityListener는 이 과정에서 발생하는 이벤트 상황을 원하는 바에 따라 읽어오겠다는 이야기이다. 

- 그런데 잠시 고민해야 하는 부분이 더 있다.

- @EntityListener를 저렇게 표시만 해둔다고 쓸 수는 없단다.

   Listener는 말 그대로 이벤트가 발생할 때까지 귀기울여 대기하는 녀석이기에,

   대기할 수 있게 미리 DI Container에 넣어주어야 한다.

 

|  AuditingEntityConfig 클래스를 만들기

- 우리는 EntityListener중에서도 AuditingEntityListener를 쓰려고 한다.

- 그리고 앞서서 스프링은 처음에 프로그램을 시작할 때, DI container안에 의존성을 주입한다고 했으며,

- 리스너는 미리 이 container안에 넣어서 대기시켜주어야한다고 했다.

* DI container에 의존성을 주입하려면 @Configuration을 붙인 설정 클래스 안에서 @Bean을 불려오는 것으로 할 수 있는데

   이 밖에도 @Component 등이 @Bean을 상속하여 붙이는 순간 시작 시 자동으로 의존성이 주입되기도 했다.

- 리스너를 대기시키기 위해 Config 클래스를 하나 만들고 @Configuration과 @EnableJpaAuditing을 붙인다.

@Configuration
@EnableJpaAuditing
public class JpaAuditingConfig {

}

 

|  정리

- JPA에서 말하는 영속성이란, DB에 영구적으로 반영되었음을 의미한다.

- @Entity, @EntityListener, @Id, @GeneratedValue 에는 이러한 영속성의 의미가 패키지명에 담겨 있다.

- @EntityListener(AuditingEntityListener.class)는 Entity에 create/update 이벤트가 발생하는 시간을 알려준다.
  - Entity 내부의 맴버에 @CreatedDate와 @LastModifiedDate를 추가하고,
  - JpaAuditingConfig 클래스를 만든 후 @Configuration과 @EnableJpaAuditing을 추가하여 사용한다.

 

 

 

[ 참조 및 출처 ]

부트캠프 수업 후 정리

JPA persistence context https://victorydntmd.tistory.com/207

Entity Listeners https://wave1994.tistory.com/161

+ Recent posts