인프런/실전! 스프링부트와 JPA활용1

1)프로젝트생성,설정, 테스트

backend dev 2023. 1. 12.

https://start.spring.io/

다음과 같이 필요한 라이브러리들 추가하고 Generate해서 프로젝트 파일을 받는다.

 

롬복설치 시 주의사항

플러그인에 롬복깔려있는지 확인
반드시 체크해야함.

 

View 환경설정

타임리프는 라이브러리 설치만하면 스프링부트가 알아서 환경설정 다 해준다.

 

타임리프 동작확인하기 

HelloController

@Controller
public class HelloController {

    @GetMapping("hello") //@GetMapping은  @RequestMapping(method = RequestMethod.GET)라는 어노테이션을 포함하고있다, hello라는 request가 들어오면 아래 메소드실행
    public String hello(Model model) { //모델이라는것에 데이터를 실어서 뷰에 넘길수있다.
        model.addAttribute("data","hello!!");// 모델에 속성이름, 속성값을 담는다.
        return "hello"; //hello.html과 같은 뜻이다. hello라는 html로 이동한다는것이고 이동하면서 model을 같이 전달하는것 같다. 해당 html은 resources -> templates에 저장하는게 관례
        //@ResponseBody를 썼다면 return에 저 값은 그냥 문자열값으로 전달됬을것이다.
    }
}

스프링부트가 이런식으로 매핑을 하기에 return hello라고해도 hello.html을 잘 찾는다.

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <title>Hello</title>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p th:text="'안녕하세요. ' + ${data}" >안녕하세요. 손님</p>
</body></html>

${data} 부분에 모델에 실은 데이터가 들어간다.

model에 담은 hello!!값이 잘 적용된걸 확인가능

 

 

implementation 'org.springframework.boot:spring-boot-devtools'

html파일을 수정하고 인텔리제이의 build -> recompile 해주면 서버를 다시껐다킬필요없이 서버가 띄어진상태에서도 수정사항을 확인할 수 있다.

 

h2 데이터베이스 설치

 

h2) h2 데이터베이스 설정 초기화하기, 시작하기

이미 h2를 사용했던적이 있는데 다 지우고 다시 설정하고 싶다면, db파일을 삭제해주고 h2설정을 초기화해줘야한다. C:\Users\사용자이름의 위치로 가서 다음과 같은 databasefile을 찾아 삭제한다. 같

keeeeeepgoing.tistory.com

 

프로젝트설정파일

application.properties가 기본으로 생성되어있다.

application.yml을 쓰던지 application.properties쓰던지 둘중 하나만 사용하면되는데 여기서는

application.yml을 사용한다 (기존에 있는 application.properties는 삭제한다)

 

둘다 프로젝트설정을 적는곳이긴한데 yml은 계층적으로 확인가능해서 편리하다고 한다.

 

 

모르는거랑 찾아볼것은

 

Spring makes Java simple.

Level up your Java code and explore what Spring can do for you.

spring.io

에서 공부하면 된다.

 

 

application.yml

spring:
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:tcp://localhost/~/jpashop
    username: sa
    password: 123

#src/main/resources/META-INF/persistence.xml에서 진행했던 jpa 설정을  Spring Boot를 사용하는 경우 application.yml에서 할 수 있다.
  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
 #       show_sql: true System.out에 로그를 남기므로 사용 X 아래의 logging으로 남기면된다.
        format_sql: true

#        src/main/resources/META-INF/persistence.xml에서 진행했던 jpa 설정했던 아래와 같은걸 application.yml에서는 위처럼 진행
#        <property name="hibernate.show_sql" value="true"/>
#        <property name="hibernate.format_sql" value="true"/>
#        <property name="hibernate.use_sql_comments"  value="true"/>

logging:
  level:
    org.hibernate.SQL: debug # jpa hibernate가 생성하는 sql이 다 보이게끔 (로거를 통해)

 

 

 

 

동작 테스트

member.java 생성

@Entity // JPA를 사용해서 테이블과 매핑할 클래스는 @Entity 어노테이션을 필수로 붙여야 한다. @Entity가 붙은 클래스는 JPA가 관리하는것으로 엔티티라 부른다.
@Getter @Setter // 롬복을 이용해서 게터세터 생성
public class Member {
    @Id @GeneratedValue  // @Id 애노테이션은 JPA 엔티티 객체의 식별자로 사용할 필드(기본키)에 적용 , 데이터베이스가 AutoIncrement해서 기본키를 생성해주는걸 원하면 @GeneratedValue로 설정(추가 코드로 전략 선택가능)
    private Long id;
    private String name;

}

 

MemberRepository .java 생성

@Repository
public class MemberRepository {

    @PersistenceContext // 해당 어노테이션이 있으면 스프링이 엔티티매니저를 주입해준다.
    private EntityManager em; // 엔티티매니저 팩토리를 만들고 해당 팩토리로 엔티티 매니저를 생성하는 과정은 스프링부트가 알아서 해준다.

    public Long save(Member member) { // 저장
        em.persist(member);
        return member.getId(); // 쿼리로 인해 멤버를 테이블에 생성하고 난뒤 생성된 아이디를 넣어주기 때문에 사용가능
    }

    public Member find(Long id) { // 조회
        return em.find(Member.class, id);
    }
}

 

 

 

 

 

커맨드와 쿼리를 분리하라

public Long save(Member member) { // 저장
    em.persist(member);
    return member.getId(); // 쿼리로 인해 멤버를 테이블에 생성하고 난뒤 생성된 아이디를 넣어주기 때문에 사용가능
}

해당 메소드는 멤버를 저장하는 커맨드이다. 그러므로 멤버나 아이디를 반환하지않고 void가 맞지만 

아이디를 반환해주면 조회할때 사용할수있으므로 아이디정도는 반환한다.

 

 


GPT의 답변

"커맨드와 쿼리를 분리하라"는 소프트웨어 설계 원칙 중 하나로, 커맨드(Command)와 쿼리(Query)를 분리하여 시스템의 각 책임을 명확히 하자는 것입니다. 이는 CQRS(Command Query Responsibility Segregation) 패턴과 관련이 있습니다. 이 패턴은 시스템을 두 가지 작업으로 나누는 것입니다:

1. **커맨드(Command)**: 시스템의 상태를 변경하는 작업. 예를 들어, 데이터베이스에 데이터를 추가, 수정, 삭제하는 작업이 여기에 해당됩니다.
2. **쿼리(Query)**: 시스템의 상태를 조회하는 작업. 즉, 데이터를 읽는 작업입니다.

이 원칙을 따르면, 상태 변경 작업은 어떠한 데이터도 반환하지 않아야 하고, 조회 작업은 상태를 변경하지 않아야 합니다.

이제 주어진 메서드를 살펴봅시다:

```java
public Long save(Member member) {
    em.persist(member);
    return member.getId();
}
```

이 메서드는 멤버 객체를 데이터베이스에 저장하고, 저장된 멤버의 ID를 반환합니다. 이 메서드가 커맨드와 쿼리를 분리하라는 원칙을 어떻게 위반하고 있는지, 그리고 이를 어떻게 이해하고 적용할 수 있는지 설명하겠습니다.

### 커맨드와 쿼리를 분리하는 이유

- **커맨드**: 상태를 변경하는 작업이므로, 수행 결과를 반환하지 않는 것이 원칙입니다. 예를 들어, `void` 메서드를 사용하는 것이 일반적입니다.
- **쿼리**: 시스템의 상태를 조회하는 작업이므로, 상태 변경 없이 데이터를 반환하는 것이 원칙입니다.

### 주어진 메서드 분석

메서드 `save`는 두 가지 역할을 합니다:
1. **멤버를 저장하는 커맨드 역할**: `em.persist(member)`를 통해 상태를 변경합니다.
2. **멤버의 ID를 반환하는 쿼리 역할**: `return member.getId()`를 통해 데이터를 조회합니다.

### 아이디를 반환하는 이유

위 원칙에 따르면, `save` 메서드는 상태 변경 작업이므로 `void`를 반환하는 것이 맞습니다. 하지만, 실용적인 측면에서 ID를 반환하는 것은 다음과 같은 이유로 유용할 수 있습니다:

- **후속 작업을 위한 편의성**: 멤버를 저장한 후, 저장된 멤버의 ID를 통해 추가적인 작업을 바로 수행할 수 있습니다. 예를 들어, 저장된 멤버를 바로 조회하거나, 다른 시스템에서 이 ID를 사용하여 참조할 수 있습니다.

### 결론

CQRS 원칙을 엄격하게 적용하면, 상태를 변경하는 메서드는 아무것도 반환하지 않고, 상태를 조회하는 메서드가 별도로 존재해야 합니다. 그러나 현실적인 이유로, 특히 ID와 같은 최소한의 정보를 반환하는 것은 실용적일 수 있습니다.

따라서, 다음과 같이 설계할 수 있습니다:

```java
// 커맨드 메서드: 멤버를 저장합니다. 상태를 변경하는 작업입니다.
public void save(Member member) {
    em.persist(member);
}

// 쿼리 메서드: 멤버의 ID를 조회합니다. 상태를 변경하지 않습니다.
public Long getMemberId(Member member) {
    return member.getId();
}
```

이 경우, 멤버를 저장하고 ID를 조회하는 두 가지 책임을 분리하여 각각의 메서드가 하나의 역할만 수행하도록 합니다. 그러나, 필요에 따라 ID를 반환하는 메서드를 유지하는 것도 가능합니다:

```java
public Long save(Member member) {
    em.persist(member);
    return member.getId();
}
```

이 방식은 현실적인 필요를 반영한 타협으로, ID를 반환하여 후속 작업을 용이하게 하는 것입니다. CQRS 원칙을 엄격히 지키는 것과 실용적인 접근 사이의 균형을 맞추는 것이 중요합니다.


테스트

@Transactional // 트랜잭션없으면 JPA가 실행되지않는다. [ 정확히는 엔티티매니저가.] + 테스트가 성공해도 롤백을 하기 위함.
@SpringBootTest // 스프링을 실행해서 테스트 하기 위함.
class MemberRepositoryTest {

    @Autowired // 테스트니까 간편하게 필드 주입
    MemberRepository memberRepository;


    @Test
    public void testMember() {
        //given
        Member member = new Member();
        member.setUsername("memberA");
        //when
        Long savedId = memberRepository.save(member);
        Member findMember = memberRepository.find(savedId);

        //then
        Assertions.assertThat(findMember.getId()).isEqualTo(member.getId()); // assertJ꺼를 사용해야한다.
        Assertions.assertThat(findMember.getUsername()).isEqualTo(member.getUsername());
    }



}
@Transactional // 트랜잭션없으면 JPA가 실행되지않는다. [ 정확히는 엔티티매니저가.]+ 테스트가 성공해도 롤백을 하기 위함.

테스트가 성공해도 해당 데이터가 저장,수정,삭제 등이 되면 안되므로 롤백해야한다.

테스트클래스레벨에 해당 어노테이션을 붙이면 성공해도 롤백된다. [ 해당 메소드에만 적용할거면 메소드레벨에 붙인다.]

 

실제 DB에 반영되는지 확인하고싶다면

@Rollback(false)

를 클래스레벨에 붙여주면 된다. [ 해당 메소드에만 적용할거면 메소드레벨에 붙인다.]

 

assertThat(findMember).isEqualTo(member);

-> 테스트 결과 : 성공

 

save -> persist 할때 영속성컨테이너에서 관리되므로

find 할때 실제 SQL쿼리가 실행되는것이 아닌 1차캐시에서 가져오는것이므로

둘은 같은 엔티티객체이다.

 

 

 

 

참고

스프링 부트를 통해 복잡한 설정이 다 자동화 되었다.

persistence.xml 도 없고, LocalContainerEntityManagerFactoryBean 도 없다.

스프링 부트를 통한 추가 설정은 스프링 부트 메 뉴얼을 참고하고,

스프링 부트를 사용하지 않고 순수 스프링과 JPA 설정 방법은 자바 ORM 표준 JPA 프로그래 밍 책을 참고하자.

 

 

쿼리 파라미터 로그 남기기

 

 

logging:
  level:
    org.hibernate.SQL: debug

application.*에서 다음과 같은 설정으로 jpa가 생성하는 쿼리를 확인가능하고

  jpa:
    properties:
      hibernate:
        format_sql: true

다음과 같은 설정으로 jpa가 생성하는 쿼리를 깔끔하게 설정했는데

 

 

쿼리의 파라미터는 다음과 같이 보이지않는다.

 

개발할때는 어떤 파라미터가 들어가는지 보고싶을때가있다.

 

SQL 실행 파라미터를 로그로 남겨보자

 

logging:
  level:
    org.hibernate.SQL: debug # jpa hibernate가 생성하는 sql이 다 보이게끔 (로거를 통해)
    org.hibernate.orm.jdbc.bind: trace # jpa가 생성하는 쿼리의 파라미터 보이게하기

 

?는 아직 나오지만 다음 로그에

어떤 파라미터가 매핑되었는지 확인가능하다.

 

logging.level:
  org.hibernate.SQL: debug
  org.hibernate.type: trace

이렇게 해도 가능 

외부 라이브러리 사용해서 확인하는 방법

[스프링 부트를 사용하면 이 라이브러리만 추가하면 된다]

 

GitHub - gavlyukovskiy/spring-boot-data-source-decorator: Spring Boot integration with p6spy, datasource-proxy, flexy-pool and s

Spring Boot integration with p6spy, datasource-proxy, flexy-pool and spring-cloud-sleuth - gavlyukovskiy/spring-boot-data-source-decorator

github.com

implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'

 

참고: 쿼리 파라미터를 로그로 남기는 외부 라이브러리는 시스템 자원을 사용하므로, 개발 단계에서는 편하게 사 용해도 된다. 하지만 운영시스템에 적용하려면 꼭 성능테스트를 하고 사용하는 것이 좋다.

사용 결과

 

댓글