반응형
  • 301(Permanently Moved)
    • 요청한 URL에 대한 HTTP 요청의 처리 책임이 영구적으로 Location 헤더에 반환된 URL 로 이전되었다는 응답.
    • 영구적으로 이전되었으므로 브라우저는 이 응답을 캐시한다.
    • 추후 같은 URL 로 요청시 브라우저는 캐시된 원래 URL 로 요청을 보낸다.
  • 302 (Found)
    • 요청한 URL 이 "일시적으로" Location 헤더가 지정하는 URL 에 의해 처리되어야 한다는 응답.
    • 클라이언트 요청은 항상 단축 URL 서버에 먼저 보내진 후 원래 URL 로 리다이렉션이 된다.

 

728x90
반응형
반응형
  • 컬랙션 객체임을 JPA 에 알려주는 Annotation.
    @Entity 
    public class Person { 
    	@Id 
        private Long id; 
        private String email; 
        
        @ElementCollection 
        @CollectionTable( name = "address", joinColumns = @JoinColumn(name = "person_id") ) 
        List<AddressInfo> addressInfoList = new ArrayList<>(); 
    }
  • Entity 와 라이프 싸이클을 같이 하며 독립적으로 사용 불가능 하다.
  • 부모 Entity가 삭제될 경우 같이 삭제된다. (실제 클래스에 cascade 를 설정하는 옵션이 없다.)
  • ElementCollection의 Fetch 전략은 기본이 Lazy 이다.
  • 실제 테이블은 FK 를 이용해서 생성된다.
    Hibernate: create table address (person_id bigint not null, address1 varchar(255), address2 varchar(255), zip_code varchar(255))
    Hibernate: create table person (id bigint not null, email varchar(255), primary key (id))
    Hibernate: alter table address add constraint FK81ihijcn1kdfwffke0c0sjqeb foreign key (person_id) references person
  • CollectionTable Annocation 을 사용하지 않을 경우에는 다음과 같이 테이블이 생성된다.
    Hibernate: create table person_address_info_list (person_id bigint not null, address1 varchar(255), address2 varchar(255), zip_code varchar(255))
728x90
반응형
반응형

JPA 가 엔티티 데이터에 접근하는 방식을 지정한다.

1. AccessType.FIELD : 필드에 직접 접근한다.

@Access(AccessType.FIELD)
private String address1;

2. AccessType.PROPERTY : 프로퍼트로 접근한다. 

@Access(AccessType.PROPERTY)
public String getAddress2() {
	return address1 + address2;
}

3. AccessType 이 지정되지 않은 경우는 @Id 위치에 따라 지정된다.

@Entity
public class OrderInfo {
    @Id
    private Long id;
    private String address1;
    @Transient
    private String address2;

    @Access(AccessType.PROPERTY)
    public String getAddress2() {
        return address1 + address2;
    }

    public void setAddress2(String address2) {
        this.address2 = address2;
    }
}

- @Id 위치가 필드에 있기때문에 기본적으로 AccessType.FIELD 가 적용된다. AccessType.PROPERTY를 같이 적용하기 위해서는 메소드 위에 AccessType.PROPERTY 를 넣어주면 된다.

4. 기타 설명들

@Access is used to specify how JPA must access (get and set) mapped properties of the entity. If access type is set to FIELD, the values will directly be read/set on the field, bypassing getters and setters. If set to PROPERTY, the getters and setters are used to access the field value.

FIELD 로 정의하면 다이렉트로 field를 read/set 하고 PROPERTY로 설정하면 getter, setter 메소드를 통해서 접근한다.
https://stackoverflow.com/questions/19264871/what-is-the-use-of-the-access-annoation-in-jpa-means-at-the-entity-level

 

If you use field-based access, your JPA implementation uses reflection to read or write your entity attributes directly. It also expects that you place your mapping annotations on your entity attributes. If you use property-based access, you need to annotate the getter methods of your entity attributes with the required mapping annotations. Your JPA implementation then calls the getter and setter methods to access your entity attributes.

5 reasons why you should use field-based access
Better readability of your code
Omit getter or setter methods that shouldn’t be called by your application
Flexible implementation of getter and setter methods
No need to mark utility methods as *@Transient*
Avoid bugs when working with proxies

https://thorben-janssen.com/access-strategies-in-jpa-and-hibernate/

 

 

728x90
반응형
반응형
Add a spring.config.import=configserver: property to your configuration.
If configuration is not required add spring.config.import=optional:configserver: instead.
To disable this check, set spring.cloud.config.enabled=false or 
spring.cloud.config.import-check.enabled=false.

Spring Cloud Config Server 랑 Client 구성하다가 위와 같은 에러를 보게 되었다... 분명 라이브러리랑 맞게 들어간것 같은데.

gradle 에 설정된 dependency는 다음과 같이 정의 했다.

implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.cloud:spring-cloud-starter-config'
implementation 'org.springframework.cloud:spring-cloud-config-client'

우선 찾아보니 내가 bootstrap.yml을 로드하는 과정에서 문제가 있던 거였다. 다음과 같이  bootstrap.yml 을 정의해놨었다.

spring:
  application:
    name: myapp
  profiles:
    active:
      dev
  cloud:
    config:
      server:
        uri: http://localhost:8888

그런데 이걸 로드하기 위해서는 라이브러리가 추가로 필요했던 거였다.

To use the legacy bootstrap way of connecting to Config Server, bootstrap must be enabled via a property or the spring-cloud-starter-bootstrap starter.

(출처 : https://docs.spring.io/spring-cloud-config/docs/current/reference/html/)

위 글에서처럼 bootstrap 방법으로 Config Server 를 접근하기 위해서는 spring-cloud-starter-bootstrap 을 추가해줘야 한다고 되어있다. 그래서 다음과 같이 build.gradle 파일에 라이브러리를 추가했다.

implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap'

그런데 위 글을 보면 뭔가 이상한 점이 있다. 잘 보면 legacy bootstrap way 라고 되어있다. 음.. legacy 라니. 그럼 이제는 이렇게 안써도 되는건가. 위 링크를 찾아가서 위로 살짝 올라가면 다음과 같은 글이 또 있다. 

Spring Boot 2.4 introduced a new way to import configuration data via the spring.config.import property. This is now the default way to bind to Config Server.
To optionally connect to config server set the following in application.properties:
application.propertiesspring.config.import=optional:configserver:
This will connect to the Config Server at the default location of "http://localhost:8888". Removing the optional: prefix will cause the Config Client to fail if it is unable to connect to Config Server. To change the location of Config Server either set spring.cloud.config.uri or add the url to the spring.config.import statement such as, spring.config.import=optional:configserver:http://myhost:8888. The location in the import property has precedence over the uri property.

정리하면 이렇다. 

1. bootstrap.yml 을 이용해서 Config Server 를 찾는 경우

  • spring-cloud-starter-bootstrap 라이브러리가 필요하다.
  • spring-cloud-starter-bootstrap 는 spring.cloud.bootstrap.enabled=true 로 설정하면서 bootstrap 에 정의 해놓은 spring.cloud.config.uri 를 통해 Config Server 를 찾는다.

2. bootstrap.yml 을 이용하지 않는 경우

  • application.yml 파일에  spring.config.import=optional:configserver 라고 정의를 하면 default 로 http://localhost:8888 주소로 Config Server 를 찾는다.
  • 주소를 변경하고 싶으면 spring.config.import=optional:configserver:http://host:port 로 정의해주면 된다.

그래서 난 2번 방법으로 application.yml 파일에 다음과 같이 정의했다.

spring:
  application:
    name: tuja
  profiles:
    active: dev
  config:
    import: optional:configserver:http://localhost:8888

 

728x90
반응형
반응형

log4j 취약점 사태에 따라서 프로젝트에 log4j 라이브러리를 변경해야 했다.

실제 프로젝트에서는 logback 을 사용중이었고 boot 버전은 2.2.4를 사용하고 있었고 spring-boot-starter-logging 을 사용중이었다.  이 라이브러리의 dependency 는 아래와 같다.

ch.qos.logback » logback-classic 1.2.3 
org.apache.logging.log4j » log4j-to-slf4j 2.12.1
org.slf4j » jul-to-slf4j 1.7.30
출처 : https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-logging/2.2.4.RELEASE

 

1. sping-boot-starter-logging 을 제외하고 log4j 라이브러리를 추가했다.

configurations {
    all {
        exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
    }
}

dependencies{
    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.15.0'
    compile group: 'org.apache.logging.log4j', name: 'log4j-to-slf4j', version: '2.15.0'
    .....
}

그리고 나서 어플리케이션을 실행시켜보니 다음과 같은 로그가 남았다.

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.4.RELEASE)

SLF4J: Failed to load class "org.slf4j.impl.StaticMDCBinder".
SLF4J: Defaulting to no-operation MDCAdapter implementation.
SLF4J: See http://www.slf4j.org/codes.html#no_static_mdc_binder for further details.

처음에는 어플리케이션이 실행이 안된건줄 알았는데 알고보니 로그가 안올라간것이었다. 그럼 왜 로그가 안나오는것일까??

일단 정확하지는 않지만 확인해본 바로는 프로젝트에서는 logback 을 사용중이었는데 sping-boot-starter-logging 라이브러리를 제외하는 바람에 logback 관련 라이브러리가 dependency 에 추가지되 않아서라는 추측을 하게 되었다. 

2. 로그백 라이브러리 추가

dependencies{
	implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.7'
    implementation group: 'ch.qos.logback', name: 'logback-core', version: '1.2.7'
    ....
}

위와같이 로그백 라이브러리를 추가했다. 어플리케이션은 정상적으로 기동이 됐다. 그런데 다른 로컬 PC 에서 다시 SLF4J 관련 메세지가 나서 다시 수정을 했다.

3. sping-boot-starter-logging 제외 취소하고 log4j 만 추가.

configurations {
    all {
        exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
    }
}

1번에서 추가했던 위 부분을 삭제하고 2번에 추가했던 로그백 라이브러리도 삭제를 했다. 결과적으로는 log4j 라이브러리만 추가된 상황이다. 실제로 dependency를 확인해보면 다음과 같이 변경이 되어있다.

Gradle: org.apache.logging.log4j:log4j-api:2.15.0
Gradle: org.apache.logging.log4j:log4j-to-slf4j:2.15.0

어플리케이션도 정상적으로 잘 기동이 되었다.

이것때문에 구글에서 SLF4J 관련해서 계속 검색 하면서 삽질했는데 간단하게 해결이 됐다. ㅠㅠ 

 

728x90
반응형
반응형

2021/02/17 - [Development/Java] - Spring Boot Test Case 작성에 대한 생각 - Service Test

2021/02/17 - [Development/Java] - Spring Boot Test Case 작성에 대한 생각 - Repository Test

Repository, Service 에 대한 테스트를 살펴봤으니 이제 Controller 테스트를 확인해보자. Controller Test 에는 @WebMvcTest 를 사용했다. Controller 는 확인해야 할 부분이 다음과 같다. 

1. request 를 요청한 url 과 파라메터가 정확한지 여부.
2. 정상 처리 되었을데 요구한 응답을 보내주는지.
3. 비정상일때에 response 에 상태 코드가 정확히 전달 되는지. 

@RunWith(SpringRunner.class)
@WebMvcTest(UserRestController.class)
public class UserRestControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Test
    public void getUsers() throws Exception {

        when(userService.findAll())
                .thenReturn(Arrays.asList(
                        Users.builder()
                                .email("test@test.com")
                                .name("test")
                                .status(UserStatus.APPLIED)
                                .build()
                ));

        this.mockMvc.perform(get("/users"))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("@[0].email").value("test@test.com"));

    }
}

테스트케이스를 통해서 response 로 받는 내용과 상태 코드, 그리고 body 에 있는 내용들을 확인 해 볼 수 있다. 

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json"]
     Content type = application/json
             Body = [{"email":"test@test.com","name":"test","userStatus":"APPLIED"}]
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

그리고 print() 를 했기 때문에 콘솔 창에 위와 같은 결과도 같이 확인 해 볼 수 있다. 

그리고 잘못된 결과값이 나올 경우에도 response 가 잘 리턴 되는지 확인해 봐야 한다.

@Test
public void getUserNotFoundException() throws Exception {

  when(userService.findUserById("test11@test.com"))
    .thenThrow(
      new RuntimeException("User not found")
  );

  this.mockMvc.perform(get("/users/test11@test.com"))
    .andDo(print())
    .andExpect(status().isBadRequest());
}


위 테스트는 파라메터로 "test11@test.com" 을 보냈으나 사용자가 존재하지 않을때 400 error 와 메세지를 보내는 경우이다. 

MockHttpServletResponse:
           Status = 400
    Error message = null
          Headers = [Content-Type:"text/plain;charset=UTF-8", Content-Length:"14"]
     Content type = text/plain;charset=UTF-8
             Body = User not found
    Forwarded URL = null
   Redirected URL = null

결과로 400 에러가 전달 되며 Body 에는 메세지가 써있다. 

Controller 테스트에서는 고려해야 할 것들이 많은것 같다. request 를 받고 response 를 보내야 하는 곳이기 때문에 파라메터에 대한 검증과 상태코드, response 값과 message 들이 잘 구성되어있어야 한다. Status 코드가 200이 떨어졌을때 보다는 200이 아닌경우의 상황을 더 많이 고려해야 되지 않을까 생각이 된다. 

주의사항) 제 생각을 기준으로 작성하고 만든 소스코드이고 의견이기 때문에 틀린 부분이 있을수 있습니다.

728x90
반응형
반응형

 

2021/02/17 - [Development/Java] - Spring Boot Test Case 작성에 대한 생각 - Repository Test

지난 글에 이어 이번에는 Service 테스트에 대해서 적어보려 한다. 아래는 내가 작성한  UserService  에 대한 테스트케이스 이다.

@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {


    private UserService userService;

    @Mock
    private UserRepository userRepository;

    private Users user1;

    @Before
    public void setup(){

        userService = new UserService(userRepository);

        user1 = Users.builder()
                .email("test01@test.com")
                .name("test01")
                .status(UserStatus.APPLIED)
                .build();
    }

    @Test
    public void findUserById(){
        when(userRepository.findById("test01@test.com")).thenReturn(Optional.of(user1));
        Users user = userService.findUserById("test01@test.com");

        verify(userRepository).findById("test01@test.com");
        Assert.assertNotNull(user);
        Assert.assertEquals("test01@test.com", user.getEmail());

    }
}

@Before 에서 UserService  를 생성해주고 Users 객체도 만들어 줬다. 그리고  UserRepository 는 Mock 으로 정의해줬다. 

실제 테스트케이스에는 when() 을 이용해서 UserRepository 가 실행됐을 때 미리 정의해둔 user1 객체를 리턴하게 만들었다. 그리고 verify 를 이용해서 실제 실행 여부를 확인했다. 

UserRepository를 Mock 으로 정의해서 테스트 케이스를 만들었다. 이유는 이미 repository 에 관련된 테스트는 repository test 에서 수행했다고 가정을 했기 때문에 굳이 repository를 실제 bean 으로 주입하고 DB 를 연결할 필요가 없다고 생각을 했다. Service 테스트케이스에서 중점적으로 체크해야 할 내용들은 조건문 처리, Exception  처리 등이 있을것 같다. 아래 코드는 Exception 에 대한 테스트 코드이다.

@Test(expected = RuntimeException.class)
public void findUserByIdException(){
  when(userRepository.findById("test011@test.com")).thenThrow(new RuntimeException("User not found"));
  userService.findUserById("test011@test.com");
}

파라메터의 null 체크 같은 경우는 좀 생각을 해봐야 할것 같다. 전에는 null 체크는 무조건 메소드 내에서 한다라고 생각을 했었는데 그렇게 코드를 짜다보니 Controller 에서 이미 null 체크를 한 파라메터를 메소드 내부에서 또 체크하는 경우가 생겼다. 지금 생각하기에는 파라메터를 넘겨주기 전에 null 체크를 해야 할것 같다는 생각이 든다. 

주의사항) 제 생각을 기준으로 작성하고 만든 소스코드이고 의견이기 때문에 틀린 부분이 있을수 있습니다.

 

728x90
반응형
반응형

테스트 케이스를 작성을 할때 내가 어렵게 생각했던것은 2가지 이다.

1. Mock 으로 테스트케이스를 작성할 것인가? 아니면 실제 데이터를 가지고 작성할 것인다.

2. 테스트용 DB를 따로 둬야 할까?

이 2가지가 항상 풀리지 않는 난제 같았다. 그런데 갑자기 뭔가 기준을 정해서 하면 되지 않을까 라는 생각이 들었다. 어떻게 보면 당연한 이야기 이긴 하지만. 테스트케이스를 작성하려는 클래스들의 역할에 생각해 보고 그에 맞는 테스트 케이스를 작성하면 되는 것이다. 

Spring Boot 프로젝트를 보면 테스트케이스를 작성하는 클래스들로 다음과 같은 것들을 꼽을 수 있다.

1. Repository
2. Service
3. Controller
4. RestApiController

그럼 우선 이번 글에서는 Repository 에 대해서만 살펴보려고 한다. Repository 는 실제로 DB 와 연결되어서 CRUD를 할 수 있는 클래스이다. Repository 클래스에 정의되어 있는 메소드를 통해서 내가 원하는 데이터를 가져올 수 있는지, 아니면 수정이 되는지 여부를 확인 하는 테스트를 작성하면 된다. 그리고 이부분은 결국 DB의 데이터가 필요하다.  Mock 으로 하기에는 실제 데이터를 가져오는지 여부가 의심(?) 스럽기 때문이다. 

@DataJpaTest
@RunWith(SpringRunner.class)
public class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    private Users users;
    @Before
    public void setup(){
        users = Users.builder()
                .email("test@test.com")
                .name("test")
                .status(UserStatus.APPLIED)
                .build();

        userRepository.save(users);
    }

    @Test
    public void getUsers(){
        List<Users> all = userRepository.findAll();
        Assert.assertNotNull(all);
        Assert.assertEquals(1, all.size());
    }

    @Test
    public void getUsersById(){
        Optional<Users> byId = userRepository.findById("test@test.com");
        Assert.assertNotNull(byId);
        Assert.assertEquals("test", byId.get().getName());
    }
}

@DataJpaTest 를 사용하면 테스트케이스 실행시에 MemoryDB 를 사용해서 실행을 한다.  테스트케이스를 실행해 보면 다음과 같은 로그를 볼 수 있다. 

Hibernate: drop table if exists users CASCADE 
Hibernate: create table users (email varchar(100) not null, name varchar(100) not null, user_status integer not null, primary key (email))
2021-02-17 10:56:06.114  INFO 2856 --- [    Test worker] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2021-02-17 10:56:06.120  INFO 2856 --- [    Test worker] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2021-02-17 10:56:06.224  INFO 2856 --- [    Test worker] c.p.common.user.web.UserRepositoryTest   : Started UserRepositoryTest in 2.808 seconds (JVM running for 3.635)
2021-02-17 10:56:06.293  INFO 2856 --- [    Test worker] o.s.t.c.transaction.TransactionContext   : Began transaction (1) for test context [DefaultTestContext@588bc70b testClass = UserRepositoryTest, testInstance = com.polpid.common.user.web.UserRepositoryTest@4f8e0671, testMethod = getUsers@UserRepositoryTest, testException = [null], mergedContextConfiguration = [MergedContextConfiguration@34cf76 testClass = UserRepositoryTest, locations = '{}', classes = '{class com.polpid.common.user.SpringCommonUserApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.autoconfigure.OverrideAutoConfigurationContextCustomizerFactory$DisableAutoConfigurationContextCustomizer@127dfcc0, org.springframework.boot.test.autoconfigure.filter.TypeExcludeFiltersContextCustomizer@351584c0, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@d5d4db2d, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@5948c9fa, [ImportsContextCustomizer@efe207e key = [org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration, org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration, org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration, org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration, org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration, org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration, org.springframework.boot.test.autoconfigure.jdbc.TestDatabaseAutoConfiguration, org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManagerAutoConfiguration]], org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@61218ded, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@e767673, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.context.SpringBootTestArgs@1], contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map[[empty]]]; transaction manager [org.springframework.orm.jpa.JpaTransactionManager@4bc55dd0]; rollback [true]
Hibernate: select users0_.email as email1_0_0_, users0_.name as name2_0_0_, users0_.user_status as user_sta3_0_0_ from users users0_ where users0_.email=?
Hibernate: insert into users (name, user_status, email) values (?, ?, ?)
Hibernate: select users0_.email as email1_0_, users0_.name as name2_0_, users0_.user_status as user_sta3_0_ from users users0_
2021-02-17 10:56:06.747  INFO 2856 --- [    Test worker] o.s.t.c.transaction.TransactionContext   : Rolled back transaction for test: [DefaultTestContext@588bc70b testClass = UserRepositoryTest, testInstance = com.polpid.common.user.web.UserRepositoryTest@4f8e0671, testMethod = getUsers@UserRepositoryTest, testException = [null], mergedContextConfiguration = [MergedContextConfiguration@34cf76 testClass = UserRepositoryTest, locations = '{}', classes = '{class com.polpid.common.user.SpringCommonUserApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.autoconfigure.OverrideAutoConfigurationContextCustomizerFactory$DisableAutoConfigurationContextCustomizer@127dfcc0, org.springframework.boot.test.autoconfigure.filter.TypeExcludeFiltersContextCustomizer@351584c0, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@d5d4db2d, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@5948c9fa, [ImportsContextCustomizer@efe207e key = [org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration, org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration, org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration, org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration, org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration, org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration, org.springframework.boot.test.autoconfigure.jdbc.TestDatabaseAutoConfiguration, org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManagerAutoConfiguration]], org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@61218ded, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@e767673, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.context.SpringBootTestArgs@1], contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map[[empty]]]
2021-02-17 10:56:06.755  INFO 2856 --- [extShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2021-02-17 10:56:06.755  INFO 2856 --- [extShutdownHook] .SchemaDropperImpl$DelayedDropActionImpl : HHH000477: Starting delayed evictData of schema as part of SessionFactory shut-down'
Hibernate: drop table if exists users CASCADE 

 

테이블을 생성하고 Before 에 정의 되어있는 데로 데이터를 insert 한후 테스트 가 종료되면 테이블을 Drop 한다. 이정도면 Repository에 정의 되어있는 메소드가 내가 생각한대로 동작을 하는지 검증하는데는 크게 문제가 없다. 실제 DB에서 가져오도록 설정도 가능하지만 내 기준에서 테스트는 실제 DB와는 분리 되어야 한다고 생각을 하기 때문에 메모리 DB가 적합한것 같다.

한가지 주의할 점은 도메인을 정의할때 필드를 DB의 예약어와 동일하게 작성을 하면 에러가 날 수 있다. 예를 들어 order 라는 필드가 있으면 테이블이 생성될때 에러가 난다. 

org.hibernate.tool.schema.spi.CommandAcceptanceException: Error executing DDL "create table users (email varchar(100) not null, name varchar(100) not null, order integer, user_status integer not null, primary key (email))" via JDBC Statement
	at org.hibernate.tool.schema.internal.exec.GenerationTargetToDatabase.accept(GenerationTargetToDatabase.java:67) ~[hibernate-core-5.4.18.Final.jar:5.4.18.Final]
	at org.hibernate.tool.schema.internal.SchemaCreatorImpl.applySqlString(SchemaCreatorImpl.java:439) [hibernate-core-5.4.18.Final.jar:5.4.18.Final]
	at org.hibernate.tool.schema.internal.SchemaCreatorImpl.applySqlStrings(SchemaCreatorImpl.java:423) [hibernate-core-5.4.18.Final.jar:5.4.18.Final]
	at org.hibernate.tool.schema.internal.SchemaCreatorImpl.createFromMetadata(SchemaCreatorImpl.java:314) [hibernate-core-5.4.18.Final.jar:5.4.18.Final]
	at org.hibernate.tool.schema.internal.SchemaCreatorImpl.performCreation(SchemaCreatorImpl.java:166) [hibernate-core-5.4.18.Final.jar:5.4.18.Final]
	at org.hibernate.tool.schema.internal.SchemaCreatorImpl.doCreation(SchemaCreatorImpl.java:135) [hibernate-core-5.4.18.Final.jar:5.4.18.Final]
	at org.hibernate.tool.schema.internal.SchemaCreatorImpl.doCreation(SchemaCreatorImpl.java:121) [hibernate-core-5.4.18.Final.jar:5.4.18.Final]
	at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.performDatabaseAction(SchemaManagementToolCoordinator.java:156) [hibernate-core-5.4.18.Final.jar:5.4.18.Final]
	at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.process(SchemaManagementToolCoordinator.java:73) [hibernate-core-5.4.18.Final.jar:5.4.18.Final]
	at org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:316) [hibernate-core-5.4.18.Final.jar:5.4.18.Final]
	at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:469) [hibernate-core-5.4.18.Final.jar:5.4.18.Final]
	at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1259) [hibernate-core-5.4.18.Final.jar:5.4.18.Final]
	at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:58) [spring-orm-5.2.8.RELEASE.jar:5.2.8.RELEASE]
	at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:365) [spring-orm-5.2.8.RELEASE.jar:5.2.8.RELEASE]
	at org

그렇기 때문에 이런 부분들을 조심하면 된다.

Repository 테스트에 대한 결론은 메모리 DB를 사용해서 실제 CRUD 를 테스트 해보면 된다.

이제 다음 글에서는 Service 테스트에 대해서 살펴볼 예정이다.

주의사항) 제 생각을 기준으로 작성하고 만든 소스코드이고 의견이기 때문에 틀린 부분이 있을수 있습니다.

 

728x90
반응형
반응형

Spring Data Jpa 를 사용하게 되면서 전에 쓰지 않았던 paging 기능에 대해서 이야기 해보려 한다. 

Domain 을 조회하는 Repository interface를 만들때에 JpaRepository 를 상속해서 사용하는 경우가 많다. 
이 JpaRepository 를 살펴보면 내부에 이렇게 구현이 되어있다.  

public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor { 
..... 
} 

그리고 다시 PagingAndSortingRepository 를 살펴보면 다음과 같다.

public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> { 
    Iterable findAll(Sort var1); 

    Page findAll(Pageable var1); 
} 

위에서 볼수 있듯이 Page를 리턴해주는 메소드가 이미 포함되어있다. 

 

그럼 간단히 검색 조건 없이 모든 Data를 검색하는 코드를 작성해보자

 

AccountRestController.java

@GetMapping
public Page<AccountResDto> getPageableAccountList(Pageable pageable){
	return accountService.findAll(pageable);
}

AccountServiceImpl.java

@Override
public Page<AccountResDto> findAll(Pageable pageable) {
  Page<Account> accounts = accountRepository.findAll(pageable);
  return accounts.map(AccountResDto::new);
}

 

http://localhost:9000/api/v1/accounts 로 요청을 하면 다음과 같이 응답을 받을수 있다.

{
    "content": [
        {
            "loginId": "admin",
            "username": "admin",
            "email": "admin@spring.com"
        },
        {
            "loginId": "auser01",
            "username": "user",
            "email": "user01@spring.com"
        },
        {
            "loginId": "guest",
            "username": "guest",
            "email": "guest@spring.com"
        },
        {
            "loginId": "buser02",
            "username": "user",
            "email": "user02@spring.com"
        },
        {
            "loginId": "cuser03",
            "username": "user",
            "email": "user03@spring.com"
        },
        {
            "loginId": "duser04",
            "username": "user",
            "email": "user04@spring.com"
        },
        {
            "loginId": "euser05",
            "username": "user",
            "email": "user05@spring.com"
        },
        {
            "loginId": "fuser06",
            "username": "user",
            "email": "user06@spring.com"
        },
        {
            "loginId": "guser07",
            "username": "user",
            "email": "user07@spring.com"
        },
        {
            "loginId": "huser08",
            "username": "user",
            "email": "user08@spring.com"
        },
        {
            "loginId": "iuser09",
            "username": "user",
            "email": "user09@spring.com"
        },
        {
            "loginId": "juser10",
            "username": "user",
            "email": "user10@spring.com"
        }
    ],
    "pageable": {
        "sort": {
            "sorted": false,
            "unsorted": true,
            "empty": true
        },
        "offset": 0,
        "pageSize": 20,
        "pageNumber": 0,
        "paged": true,
        "unpaged": false
    },
    "last": true,
    "totalPages": 1,
    "totalElements": 12,
    "number": 0,
    "size": 20,
    "sort": {
        "sorted": false,
        "unsorted": true,
        "empty": true
    },
    "first": true,
    "numberOfElements": 12,
    "empty": false
}

리턴 결과를 잘 보면 totalPages 는 1, data 개수는 12, size 는 20 으로 되어있다. 

 

그리고 정렬된 결과를 얻고 싶으면 다음과 같이 파라메터를 추가해주면 된다.

 

http://localhost:9000/api/v1/accounts?sort=loginId,DESC

 

이렇게 하면 loginId 로 정렬된 형태의 결과를 받을수 있다. (그런데 주의할점은 컴마(,) 이후에 공백이 포함되어 있으면 에러가 난다 -_-)

 

그리고 size 도 지정할수 있는데 위 결과는 지정을 하지 않아서 기본 default 로 20 으로 설정된 경우이다.

 

마찬가지로 파라메터에 size 값을 지정해주면 지정한 값으로 size 가 정해지며 그에 따른 totalPages 값도 달라지게 된다.

 

위 코드가 포함된 소스는 아래 주소에서 볼수 있다. (단, 계속 수정을 하기 때문에 소스가 완전히 일치하지 않을수 있다. )

 

https://github.com/blusky10/study_spring

 

blusky10/study_spring

Contribute to blusky10/study_spring development by creating an account on GitHub.

github.com

 

728x90
반응형

+ Recent posts