기본 구조: User (1) ↔ (N) Post

  • User는 여러 Post를 가질 수 있다 (1:N)
  • Post는 하나의 User에만 속한다 (N:1)
  • 관계 주인은 Post (외래키를 가짐)
@Entity
@Data
@NoArgsConstructor
public class User extends AuditableEntity {

    @Id 
    @GeneratedValue
    private Long id;

    private String name;

    @OneToMany
    @JoinColumn(name = "user_id", insertable = false, updatable = false) // 읽기 전용
    private List<Post> posts  = new ArrayList<>();; // post.userId = user.id
}
@Entity
@Data
@NoArgsConstructor
public class Post extends AuditableEntity{

    @Id 
    @GeneratedValue
    private Long id;

    private String content;

    @Column(name = "user_id")
    private Long userId; // 연관 객체 대신 외래키 값만 저장
}

단방향 @OneToMany에서 @JoinColumn을 사용하는 이유

문제점: Join Table이 자동 생성됨

JPA에서 @OneToMany 단방향을 사용할 때 @JoinColumn을 명시하지 않으면,

JPA는 중간에 조인 테이블 (예: user_post) 를 생성해서 User와 Post를 연결.

이것은 일반적인 비즈니스 요구와는 맞지 않고, 불필요한 테이블과 성능 비용이 생김.

비교 정리

설정  결과
@OneToMany 단독 사용 JPA가 조인 테이블 생성 (user_post)
@OneToMany + @JoinColumn 조인 테이블 생략, 외래키 user_id가 Post에 직접 생성됨

 

'Spring' 카테고리의 다른 글

JPA - 1:1 연관 관계  (0) 2025.06.16
JPA - Entity Listener의 활용  (1) 2025.06.12
JPA  (1) 2025.06.06
Jacoco - 테스트 커버리지 확인하기  (1) 2025.04.21
JUnit  (0) 2025.04.21

일자 관련 자동 처리를 위한 Entity - EntityListener

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Data
public abstract class AuditableEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;
}

1. 1:1 단방향 관계

@Entity
@NoArgsConstructor
@Data
public class User extends AuditableEntity {

    @Id 
    @GeneratedValue
    private Long id;

    private String name;

    @OneToOne
    private Address address;
}
@Entity
@NoArgsConstructor
@Data
public class Address extends AuditableEntity {

    @Id 
    @GeneratedValue
    private Long id;

    private String city;
    private String street;
}
  • User가 관계의 주인
  • address_id 외래키가 User 테이블에 생성됨
  • Address에서는 User 참조가 없음 (단방향)

2. 양방향 관계 + mappedBy 사용

@Entity
@NoArgsConstructor
@Data
public class User extends AuditableEntity {

    @Id 
    @GeneratedValue
    private Long id;

    private String name;

    @OneToOne(mappedBy = "user")
    @ToString.Exclude 
    private Address address;
}
@Entity
@NoArgsConstructor
@Data
public class Address extends AuditableEntity {

    @Id 
    @GeneratedValue
    private Long id;

    private String city;
    private String street;

    @OneToOne
    private User user;
}
  • Address가 관계의 주인 (외래키 보유)
  • User는 mappedBy = "user"로 연관관계 주인이 아님
  • 양방향 관계이지만 외래키는 Address에 있음
  • 양방향 관계를 사용할 때 순환 참조를 조심해야한다. 따라서 사용할 이유가 있을 경우에는 한쪽에 @ToString.Exclude 를 사용하여 순환 참조를 방지

'Spring' 카테고리의 다른 글

JPA - 1:N 연관 관계  (0) 2025.06.16
JPA - Entity Listener의 활용  (1) 2025.06.12
JPA  (1) 2025.06.06
Jacoco - 테스트 커버리지 확인하기  (1) 2025.04.21
JUnit  (0) 2025.04.21

JPA 엔티티의 생명주기 이벤트(생성, 수정 등)에 반응하여 자동으로 특정 메서드를 실행할 수 있게 해주는 기능이다.

→ 핵심 목적: 반복되는 엔티티 전처리/후처리 로직을 재사용 가능하게 분리.

 

어노테이션명  호출시점
@PrePersist insert 쿼리 실행 전
@PostPersist insert 쿼리 실행 후
@PreUpdate update 쿼리 실행 전
@PostUpdate update 쿼리 실행 후
@PreRemove delete 쿼리 실행 전
@PostRemove delete 쿼리 실행 후
@PostLoad select 쿼리 실행 후
import java.time.LocalDateTime;

public interface Auditable {
		LocalDataTime getCreateAt();
		LocalDataTime getUpdateAt();

    void setCreatedAt(LocalDateTime createdAt);
    void setUpdatedAt(LocalDateTime updatedAt);
}

import jakarta.persistence.*;

import java.time.LocalDateTime;

public class AuditListener {

    @PrePersist
    public void perPersist(Object o) {
        if (o instanceof Auditable) {
            ((Auditable) o).setCreateAt(LocalDateTime.now());
            ((Auditable) o).setUpdateAt(LocalDateTime.now());
        }
    }

    @PreUpdate
    public void perUpdate(Object o) {
        if (o instanceof Auditable) {
            ((Auditable) o).setUpdateAt(LocalDateTime.now());
        }
    }
}

import jakarta.persistence.*;
import java.time.LocalDateTime;

@Entity
@NoArgsConstructor
@Data
@EntityListeners(AuditListener.class)
public class Post implements Auditable {

    @Id
    @GeneratedValue
    private Long id;

    private String title;
    
    private LocalDateTime createdAt;
    
    private LocalDateTime updatedAt;

  
    // getter/setter 생략 가능
}

 

AuditingEntityListener

Spring Boot에서 제공하는 AuditingEntityListener는 Spring Data JPA의 감사(Auditing) 기능을 자동화해주는 리스너 클래스다. 이 리스너를 통해 createdDate, lastModifiedDate, createdBy, lastModifiedBy 같은 필드를 자동으로 관리할 수 있다.

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

import java.time.LocalDateTime;

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Data
public class BaseEntity {

    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;
}
import jakarta.persistence.*;
import java.time.LocalDateTime;

@Entity
@NoArgsConstructor
@Data
@ToString(callSuper= true)
@EqualsAndHashCode(callSuper= true)
@EntityListeners(AuditListener.class)
public class Post extends BaseEntity implements Auditable {

    @Id
    @GeneratedValue
    private Long id;

    private String title;
    
}

'Spring' 카테고리의 다른 글

JPA - 1:N 연관 관계  (0) 2025.06.16
JPA - 1:1 연관 관계  (0) 2025.06.16
JPA  (1) 2025.06.06
Jacoco - 테스트 커버리지 확인하기  (1) 2025.04.21
JUnit  (0) 2025.04.21

 

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
}

spring data jpa 사용을 위해 의존성 추가

 

ORM(Object Relational Mapping)

ORM은 Object Relational Mapping의 약자로, 객체 지향 프로그래밍 언어(예: Java)에서 사용하는 객체(엔티티)와 관계형 데이터베이스(RDB)에서 사용하는 테이블 간의 불일치를 해결하고, 이 둘을 자동으로 매핑해주는 기술입니다. 즉, 자바의 객체와 데이터베이스의 테이블을 1:1로 연결해주는 중계자 역할을 하며, 개발자가 객체를 조작하면 그에 맞춰 자동으로 데이터베이스에 반영되도록 해줍니다

@GeneratedValue는 JPA(Java Persistence API)에서 엔티티 클래스의 주요 키(Primary Key) 값을 자동으로 생성하기 위해 사용하는 어노테이션입니다. 주로 @Id와 함께 사용되어, 데이터베이스에 엔티티를 저장할 때 해당 필드의 값을 자동으로 생성하도록 지시합니다

엔티티 정의

package com.example.bookmanager.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;

import java.time.LocalDateTime;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NonNull
    private String name;

    @NonNull
    private String email;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

 

전략명 설명 주요 사용 DBMS 언제 사용해야 하는가
IDENTITY DB의 auto-increment/identity 기능으로 PK 생성. DB가 직접 값 할당. MySQL, MariaDB, PostgreSQL(SERIAL), SQL Server 등 DB가 PK를 자동 증가로 관리할 때. 단순하고, 별도 시퀀스/테이블 없이 DB 기본 기능을 그대로 쓸 때 적합. 대량 배치/성능이 중요하지 않을 때.
SEQUENCE DB의 시퀀스 오브젝트에서 PK 값을 가져와 할당. Oracle, PostgreSQL, H2 등 DB가 시퀀스를 지원할 때. 대량 데이터 삽입, JPA 배치 처리 등 고성능이 필요할 때. 시퀀스를 커스터마이즈할 때.
TABLE 별도 테이블에 PK 값을 저장·증가시켜 관리. 모든 DBMS 시퀀스/identity 미지원 DB에서 DB 독립적 키 생성이 필요할 때. 하지만 성능 저하로 대규모 트랜잭션에는 비권장.
AUTO JPA 구현체가 DB에 맞는 전략을 자동 선택. 모든 DBMS DB 종류가 바뀔 수 있거나, 특별한 요구사항이 없을 때. 그러나 동작이 DB/버전에 따라 달라질 수 있어 일관성이 중요할 때.

요약

  • IDENTITY: DB가 PK를 자동 증가로 관리할 때(간단, 소규모, DB에 맞춘 설계).
  • SEQUENCE: 시퀀스 지원 DB에서 성능과 확장성을 원할 때(대량 데이터, 커스텀 시퀀스).
  • TABLE: DB 독립성이 우선이거나, 시퀀스/identity 없는 DB에서(성능 저하 감수).
  • AUTO: JPA가 알아서 결정(프로토타입, DB 변경 가능성 있을 때, 단 일관성 주의).

Repository 정의

package com.example.bookmanager.repository;

import com.example.bookmanager.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}

JpaRepository<{Entity 클래스} , {PK 데이터 타입}> 을 상속 받는다.

Application.yml 설정

spring:  
	  jpa:
	    show-sql: true #jpa 사용 시 sql 로그 출력
      properties:
		      hibernate:
			        format_sql: true #  sql 로그를 sql 포맷형식으로 출력
	    defer-datasource-initialization: true
	  h2:
	    console:
	      enabled: true

Spring Boot 2.5 이상부터는 기본적으로 data.sql과 같은 SQL 스크립트가 Hibernate(JPA)의 DDL(테이블 생성 등) 작업보다 먼저 실행된다. 이 때문에 Hibernate가 아직 초기화 되지 않았는데 data.sql이 실행되어 에러가 발생할 수 있다.

spring.jpa.defer-datasource-initialization: true를 설정하면 Hibernate의 DDL 작업(엔티티 기반 테이블 생성 등)이 모두 끝난 뒤에 data.sql, schema.sql 등 SQL 스크립트가 실행된다

JPA 주요 메소드 및 네이밍 규칙 정리

JpaRepository가 기본적으로 제공하는 주요 메소드와 Spring Data JPA 쿼리 메소드 네이밍 규칙을 아래와 같이 정리할 수 있습니다.


주요 기본 메소드

  • save(S entity): 새로운 엔티티는 저장, 이미 존재하는 엔티티는 병합.
  • delete(T entity): 엔티티 하나 삭제.
  • findById(ID id): ID로 엔티티 하나 조회, Optional<T> 반환.
  • findAll(): 모든 엔티티 조회, 정렬(Sort)·페이징(Pageable) 지원.
  • count(): 전체 엔티티 개수 반환.
  • existsById(ID id): 해당 ID 엔티티 존재 여부 반환.
  • saveAll(Iterable<S> entities): 여러 엔티티 저장.

쿼리 메소드 네이밍 규칙 및 예시

Spring Data JPA는 메소드 이름만으로도 다양한 쿼리를 자동 생성합니다.

기능 메소드 네이밍 시작 반환 타입  예시 및 설명
조회 findBy, readBy, queryBy, getBy List<T>, Optional<T> findByName(String name)
카운트 countBy long countByAgeGreaterThan(int age)
존재여부 existsBy boolean existsByEmail(String email)
삭제 deleteBy, removeBy long, void deleteByName(String name)
중복제거 findDistinctBy List<T> findDistinctByEmail(String email)

연산자 및 키워드 조합

  • And, Or: findByNameAndAge, findByNameOrEmail
  • Between: findByAgeBetween(int start, int end)
  • LessThan, GreaterThan: findByAgeLessThan(int age), findByAgeGreaterThan(int age)
  • Like, NotLike: findByNameLike(String pattern)
  • In, NotIn: findByAgeIn(List<Integer> ages)
  • OrderBy: findByAgeOrderByNameDesc(int age)
  • IsNull, IsNotNull: findByEmailIsNull(), findByEmailIsNotNull()
  • True, False: findByActiveTrue(), findByActiveFalse()
  • IgnoreCase: findByNameIgnoreCase(String name)
  • Limit: findFirst3ByOrderByAgeDesc(), findTopByOrderByScoreAsc()
public interface UserRepository extends JpaRepository<User, Long> {
    List<User> findByName(String name);
    List<User> findByNameAndAge(String name, int age);
    List<User> findByAgeLessThan(int age);
    List<User> findByNameContaining(String part);
    List<User> findByNameOrderByAgeAsc(String name);
    List<User> findByAgeBetween(int start, int end);
}

data.sql로 사전 데이터 정의 하기

resources 하위에 data.sql을 생성하면 spring boot가 구동되면 자동으로 data.sql를 실행한다.

-- PK가 GenerationType.IDENTITY 인 경우
insert into users (`name`, `email`,`created_at`,`updated_at`) values ('jun','jun@gmail.com',now(),now());
insert into users (`name`, `email`,`created_at`,`updated_at`) values ('steve','steve@naver.com',now(),now());
insert into users (`name`, `email`,`created_at`,`updated_at`) values ('dennis','dennis@hanmail.net',now(),now());
insert into users (`name`, `email`,`created_at`,`updated_at`) values ('queen','queen@daum.com',now(),now());
insert into users (`name`, `email`,`created_at`,`updated_at`) values ('jun','jun@yahoo.com',now(),now());

JPA save() 등 ORM 코드로 데이터 삽입 (권장)

  • JPA가 시퀀스와 PK 할당을 완전히 관리하므로, 시퀀스 값 불일치, 음수 PK, 중복 오류 등 예기치 못한 문제가 발생하지 않습니다.
  • 테스트 데이터가 필요한 경우, 테스트 코드의 @BeforeEach 또는 별도 초기화 메서드에서 save()로 엔티티를 저장합니다.
  • 예시:
  • @BeforeEach void setUp() { userRepository.save(new User("jun", "[jun@gmail.com](<mailto:jun@gmail.com>)", ...)); userRepository.save(new User("steve", "[steve@naver.com](<mailto:steve@naver.com>)", ...)); }

flush : DB 반영 시점 조절. 로그상의 크게 변화 없음

delete 관련 일반 함수들은 먼저 해당 엔티티가 DB상에 있는지 각각 select쿼리로 확인 한 후 삭제 처리를 한다

delete관련 Batch 함수들은 where 속성 in 으로 확인 후 where 속성 or 절로 한번에 삭제 처리를 한다.

allInBatch 같은 경우는 select 쿼리를 거치지 않고 바로 삭제 처리를 한다.

 

pagenation 기능이 필요한 경우 JPA find 함수의 인자로 PageRequest.of(페이지번호, 페이지 사이즈)를 넣어주면 Page<Entitiy> 객체를 반환 받아서 활용할 수 있다.

@Test
    void crud(){
        Page<User> users = userRepository.findAll(PageRequest.of(1,3));

        users.forEach(System.out::println);

    }
   @Test
    void crud(){
        Page<User> users = userRepository.findAll(PageRequest.of(1,3));

        System.out.println("pages :" + users);
        System.out.println("totalElements :" + users.getTotalElements());
        System.out.println("totalPages :" + users.getTotalPages());
        System.out.println("Number of elements :" + users.getNumberOfElements());
        System.out.println("sort :"+users.getSort());
        System.out.println("size : "+users.getSize());

        users.getContent().forEach(System.out::println);
    }

// 콘솔 결과
pages :Page 2 of 2 containing com.example.bookmanager.domain.User instances
totalElements :5
totalPages :2
Number of elements :2
sort :UNSORTED
size : 3

QueryByExampleExecutor란?

QueryByExampleExecutor는 Spring Data JPA에서 제공하는 인터페이스로, 엔티티의 일부 필드 값(예: name, email 등)을 예시로 삼아 동적 쿼리(Example Query)를 쉽게 생성할 수 있게 해줍니다. 즉, “이런 값이 들어있는 엔티티를 찾아줘”라는 식의 검색을 코드 몇 줄로 구현할 수 있다.

위 방식은 특히 문자열에 관련된 것만 쓸 수 있다는 한계점이 있어서 조금 복잡한 쿼리를 만들 때는

Query DSL과 같은 별도의 방식으로 구현을 하게 된다.

@Test
void crud() {
    ExampleMatcher matcher = ExampleMatcher.matching()
            .withIgnorePaths("name")
            .withMatcher("email", endsWith());

    Example<User> example = Example.of(new User("j", "gmail.com"), matcher);

    userRepository.findAll(example).forEach(System.out::println);
}

주요 개념

Probe: 실제로 값을 채운 엔티티 객체(예: new User("j", "gmail.com")). 이 객체의 값이 쿼리 조건이 됩니다.

ExampleMatcher: 어떤 필드는 무시하고, 어떤 필드는 어떻게 비교할지(예: endsWith, contains 등) 지정하는 도구.

Example: Probe와 ExampleMatcher를 합쳐서, 실제로 쿼리를 만들 때 사용하는 객체.

QueryByExampleExecutor: findAll(Example<S> example) 등 Example 기반 쿼리 메서드를 제공하는 인터페이스. Spring Data JPA의 Repository가 이 인터페이스를 상속받으면 QBE 기능을 쓸 수 있습니다

'Spring' 카테고리의 다른 글

JPA - 1:1 연관 관계  (0) 2025.06.16
JPA - Entity Listener의 활용  (1) 2025.06.12
Jacoco - 테스트 커버리지 확인하기  (1) 2025.04.21
JUnit  (0) 2025.04.21
Server to Server - RestTemplate  (1) 2025.04.16

Jacoco
Java코드의 코드 커버리지를 체크 하는 라이브러리 
결과를 html, xml, csv로 확인가능

 

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.4'
    id 'io.spring.dependency-management' version '1.1.7'
    id 'jacoco' // 추가
}

bulid.gradle 에서 plugins추가

 

Gradle 창의 verification check 더블클릭

 

 

build - report-tests 폴더 안에 index.html 파일이 생성

 

다음과 같이 확인이 가능하다

 

Gradle 창의 verification jacocoTestReport 더블클릭

 

build- report-jacoco 폴더 안에 index.html 파일이 생성

 

다음과 같이 전체 코드에 대한 테스트 커버리지를 확인 할 수 있다.

'Spring' 카테고리의 다른 글

JPA - Entity Listener의 활용  (1) 2025.06.12
JPA  (1) 2025.06.06
JUnit  (0) 2025.04.21
Server to Server - RestTemplate  (1) 2025.04.16
비동기 처리하기  (1) 2025.04.15

TDD(Test Driven Development, 테스트 주도 개발)

소프트웨어 개발 방법론 중 하나로, 실제 코드 작성에 앞서 테스트 코드를 먼저 작성하고, 그 테스트를 통과하는 코드를 구현하는 개발 방식을 의미

 

단위테스트

작성한 코드가 기대하는 대로 동작을 하는지 검증 하는 절차

 

Jnit

Java기반의 단위 테스트를 위한 프레임워크

Annotation 기반으로 테스트를 지원하며, Assert를 통하여 검증

 

dependencies {
    testImplementation platform('org.junit:junit-bom:5.10.0')
    testImplementation 'org.junit.jupiter:junit-jupiter'

		// mock 관련 의존성 추가
    testImplementation("org.mockito:mockito-core:4.11.0")
    testImplementation("org.mockito:mockito-junit-jupiter:4.11.0")
}

 

@ExtendWith(MockitoExtension.class)
public class DollarCalculatorTest {

    @Mock
    public MarketApi marketApi;

    @BeforeEach
    public void init(){
		    // marketApi.connect()의 결과로 3000 반환
        Mockito.lenient().when(marketApi.connect()).thenReturn(3000);
    }

    @Test
    public void testHello() {
        System.out.println("hello");
    }

    @Test
    public void dollarTest() {
        MarketApi marketApi = new MarketApi();
        DollarCalculator dollarCalculator = new DollarCalculator(marketApi);
        dollarCalculator.init();;

        Calculator calculator = new Calculator(dollarCalculator);
        Assertions.assertEquals(22000,calculator.sum(10,10));
        Assertions.assertEquals(0,calculator.minus(10,10));
    }

    @Test
    public void mockTest() {
        DollarCalculator dollarCalculator = new DollarCalculator(marketApi);
        dollarCalculator.init();;

        Calculator calculator = new Calculator(dollarCalculator);
        Assertions.assertEquals(60000,calculator.sum(10,10));
        Assertions.assertEquals(0,calculator.minus(10,10));
    }

}

 

어노테이션  설명
@ExtendWith(MockitoExtension.class) JUnit 5에서 Mockito 프레임워크의 기능을 테스트 클래스에 통합해주는 확장 어노테이션. Mock 객체 자동 초기화 등 Mockito의 기능을 손쉽게 사용할 수 있게 해준다
@Mock Mockito에서 제공하는 어노테이션으로, 인터페이스나 클래스의 Mock(가짜) 객체를 생성한다. 실제 구현체 대신 가상의 객체를 만들어 테스트 대상 클래스의 격리를 가능하게 한다
@BeforeEach JUnit 5에서 각 테스트 메서드 실행 전에 공통 초기화 작업을 수행하기 위해 사용된다. 예를 들어 Mock 객체 초기화, 테스트 데이터 준비 등에 활용된다
@Test JUnit 5에서 해당 메서드가 테스트 메서드임을 나타내는 어노테이션. 이 어노테이션이 붙은 메서드는 JUnit이 자동으로 실행하여 코드의 특정 동작이 기대한 대로 동작하는지 검증한다
package com.example.springcalculator.component;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;

@SpringBootTest
class DollarCalculatorTest {

    @MockitoBean
    private MarketApi marketApi;

    @Autowired
    private DollarCalculator dollarCalculator;

    @Test
    public void dollarCalculatorTest() {
        Mockito.when(marketApi.connect()).thenReturn(3000);
        dollarCalculator.init();

        int sum = dollarCalculator.sum(10,10);
        int minus = dollarCalculator.minus(10,10);

        Assertions.assertEquals(60000, sum);
        Assertions.assertEquals(0, minus);
    }

}

@SpringBootTest

    Spring Boot 애플리케이션의 전체 컨텍스트(모든 Bean)를 로드하여 실제 운영 환경과 유사하게 테스트를 진행

@MockitoBean

    MarketApi 빈을 Mockito mock 객체로 대체

package com.example.springcalculator.controller;

import com.example.springcalculator.component.Calculator;
import com.example.springcalculator.component.DollarCalculator;
import com.example.springcalculator.component.MarketApi;
import com.example.springcalculator.dto.Req;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

@WebMvcTest(CalculatorApiController.class)
@Import({Calculator.class, DollarCalculator.class})
public class CalculatorApiControllerTest {

    @MockitoBean
    private MarketApi marketApi;

    @Autowired
    private MockMvc mockMvc;

    @BeforeEach
    public void init(){
        Mockito.when(marketApi.connect()).thenReturn(3000);
    }

    @Test
    public void sumTest() throws Exception {
        // <http://localhost:8080/api/sum?a=10&b=10>

        mockMvc.perform(MockMvcRequestBuilders.get("<http://localhost:8080/api/sum>")
                        .queryParam("a","10")
                        .queryParam("b","10"))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.content().string("60000"))
                .andDo(MockMvcResultHandlers.print());

    }

    @Test
    public void minusTest() throws Exception {
        // <http://localhost:8080/api/minus?a=10&b=10>

        Req req = new Req();
        req.setA(10);
        req.setB(10);

        String json = new ObjectMapper().writeValueAsString(req);

        // {"result":0,"response":{"resultCode":"OK"}}
        mockMvc.perform(MockMvcRequestBuilders.post("<http://localhost:8080/api/minus>")
                .contentType(MediaType.APPLICATION_JSON)
                .content(json))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.result").value(0))
                .andExpect(MockMvcResultMatchers.jsonPath("$.response.resultCode").value("OK"))
                .andDo(MockMvcResultHandlers.print());

    }

}

 

어노테이션  주요 목적/용도 로드되는 빈 범위  MockMvc
자동 등록
 테스트 속도/무게감 주 사용 사례  장단점 요약
@WebMvcTest 컨트롤러(Web Layer) 단위 테스트 Controller,
Web 관련 빈만
O 빠름/가벼움 컨트롤러 단위 테스트 장점: 빠르고 가볍다. Web Layer 집중.
단점: Service/Repository 등은 Mock 필요, 실제 환경과 다를 수 있음.
@SpringBootTest 전체 애플리케이션 통합 테스트 모든 스프링 빈 X (직접 추가 필요) 느림/무거움 통합 테스트, 실제 환경 검증 장점: 실제 환경과 유사, 모든 빈 사용 가능.단점: 느리고 무거움, 디버깅 어려움.
@SpringBootTest
@AutoConfigureMockMvc
전체 애플리케이션 + MockMvc 테스트 모든 스프링 빈 O 느림/무거움 통합 테스트 + MockMvc 사용 장점: 실제 환경과 유사, MockMvc로 HTTP 테스트 가능.
단점: 느리고 무거움.

 

'Spring' 카테고리의 다른 글

JPA  (1) 2025.06.06
Jacoco - 테스트 커버리지 확인하기  (1) 2025.04.21
Server to Server - RestTemplate  (1) 2025.04.16
비동기 처리하기  (1) 2025.04.15
Interceptor  (0) 2025.04.15

지금까지 살펴본 부분은 항상 Server의 입장에서 API를 제공하는 방법에 대하여 학습

Back-end에서 Client로 다른 Server와의 연결은 필수!!!

Port 설정은 resource 하위의 application.properties 파일에서

server.port = {사용할 포트 번호} 로 변경할 수 있다.

1. Server to Server - GET

Client

package com.example.client.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserResponse {
    private String name;
    private int age;
}

package com.example.client.service;

import com.example.client.dto.UserResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;

@Service
public class RestTemplateService {

    // <http://localhost/api/server/hello>
    // response
    public UserResponse hello(){
        URI uri = UriComponentsBuilder
                .fromUriString("<http://localhost:9090>")
                .path("/api/server/hello")
                .queryParam("name","jun")
                .queryParam("age",30)
                .encode()
                .build()
                .toUri();

        System.out.println(uri.toString());

        RestTemplate restTemplate = new RestTemplate();

        ResponseEntity<UserResponse> result = restTemplate.getForEntity(uri, UserResponse.class);
        System.out.println("getStatusCode : "+ result.getStatusCode());
        System.out.println("getBody : "+ result.getBody());

        return result.getBody();
    }
}

RestTemplate 클래스를 이용하여 다른 서버로 요청(GET, POST, DELETE, …) 을 보낼 수 있다.

서버와 API 표준 스펙을 정하여 DTO 클래스를 설계하여 응답을 받을 때 사용한다.

Server

package com.example.server.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private String name;
    private int age;
}

package com.example.server.controller;

import com.example.server.dto.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/server")
public class ServerApiController {

    @GetMapping("/hello")
    public User hello(@RequestParam String name ,@RequestParam int age){
        User user = new User();
        user.setName(name);
        user.setAge(age);

        return user;
    }
}

 

 

2. Server to Server - POST

Client

package com.example.client.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserRequest {
    private String name;
    private int age;
}
package com.example.client.service;

import com.example.client.dto.UserRequest;
import com.example.client.dto.UserResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;

@Service
public class RestTemplateService {

    public UserResponse post(){
        // <http://localhost:9090/api/server/user/{userId}/name/{userName}>

        URI uri = UriComponentsBuilder
                .fromUriString("<http://localhost:9090>")
                .path("/api/server/user/{userId}/name/{userName}")
                .encode()
                .build()
                .expand(100,"jun")
                .toUri();

        System.out.println(uri);

        // http body -> object -> object mapper -> json -> rest template -> http body json
        UserRequest req = new UserRequest();
        req.setAge(30);
        req.setName("jun");

        RestTemplate restTemplate = new RestTemplate();

        ResponseEntity<UserResponse> response = restTemplate.postForEntity(uri, req, UserResponse.class);

        System.out.println("getStatusCode : "+ response.getStatusCode());
        System.out.println("getHeaders : "+ response.getHeaders());
        System.out.println("getBody : "+ response.getBody());

        return response.getBody();

    }
}

RestTemplate의 postForEntity 함수를 이용하여 requstBody의 내용을 추가하여 다른 서버로 요청할 수 있다.

Server

package com.example.server.controller;

import com.example.server.dto.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
@RequestMapping("/api/server")
public class ServerApiController {

    @PostMapping("/user/{userId}/name/{userName}")
    public User post(@RequestBody User user, @PathVariable int userId, @PathVariable String userName){
        log.info("userId : {} , userName : {}",userId,userName);
        log.info("user : {}",user);

        return user;
    }
}

 

 

3. Server to Server - exchange

Client

package com.example.client.service;

import com.example.client.dto.Req;
import com.example.client.dto.UserRequest;
import com.example.client.dto.UserResponse;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;

@Service
public class RestTemplateService {

    public UserResponse exchange(){
        // <http://localhost:9090/api/server/user/{userId}/name/{userName}>

        URI uri = UriComponentsBuilder
                .fromUriString("<http://localhost:9090>")
                .path("/api/server/user/{userId}/name/{userName}")
                .encode()
                .build()
                .expand(100,"jun")
                .toUri();

        System.out.println(uri);

        // http body -> object -> object mapper -> json -> rest template -> http body json
        UserRequest req = new UserRequest();
        req.setAge(30);
        req.setName("jun");

        RequestEntity<UserRequest> requestEntity = RequestEntity
                                                .post(uri)
                                                .header("x-authorization","asdf")
                                                .header("custom-header","112233")
                                                .body(req);

        RestTemplate restTemplate = new RestTemplate();

        ResponseEntity<UserResponse> response = restTemplate.exchange(requestEntity,UserResponse.class);

        System.out.println("getStatusCode : "+ response.getStatusCode());
        System.out.println("getHeaders : "+ response.getHeaders());
        System.out.println("getBody : "+ response.getBody());

        return response.getBody();

    }

}

RestTemplate의 exchange함수를 이용하여 RequestEntity를 추가할 수 있다.

→ RequestEntity 클래스에서 httpMethod, URI, Header, Body를 설정

Server

package com.example.server.controller;

import com.example.server.dto.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
@RequestMapping("/api/server")
public class ServerApiController {

    @GetMapping("/hello")
    public User hello(@RequestParam String name ,@RequestParam int age){
        User user = new User();
        user.setName(name);
        user.setAge(age);

        return user;
    }

    @PostMapping("/user/{userId}/name/{userName}")
    public User post(@RequestBody User user,
                     @PathVariable int userId,
                     @PathVariable String userName,
                     @RequestHeader(name ="x-authorization") String authorization ,
                     @RequestHeader(name ="custom-header") String customHeader){
        log.info("userId : {} , userName : {}",userId,userName);
        log.info("x-authorization : {} , custom-header: {}",authorization,customHeader);
        log.info("user : {}",user);

        return user;
    }
}

 

4. Server to Server - GenericExchange

{
	"header": {
		"response_code" : ""
	},
	"body" : {
		"name" : "jun",
		"age" : 30

	}

위 처럼 JSON 내용이 header,body 2가지로 나뉘고 상황에 따라 body의 내용만 바뀌는 상황이 있을 수 있다.

제네릭 dto클래스를 이용하여 간단하게 구현할 수 있다.

Client

package com.example.client.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Req<T> {
    private Header header;
    private T resBody;

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Header{
        private String responseCode;
    }
}

package com.example.client.service;

import com.example.client.dto.Req;
import com.example.client.dto.UserRequest;
import com.example.client.dto.UserResponse;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;

@Service
public class RestTemplateService {

    public Req<UserResponse> genericExchange(){
        // <http://localhost:9090/api/server/user/{userId}/name/{userName}>

        URI uri = UriComponentsBuilder
                .fromUriString("<http://localhost:9090>")
                .path("/api/server/user/{userId}/name/{userName}")
                .encode()
                .build()
                .expand(100,"jun")
                .toUri();

        System.out.println(uri);

        // http body -> object -> object mapper -> json -> rest template -> http body json
        UserRequest userRequest = new UserRequest();
        userRequest.setAge(30);
        userRequest.setName("jun");

        Req<UserRequest> req = new Req<>();
        req.setHeader(new Req.Header());
        req.setResBody(userRequest);

        RequestEntity<Req<UserRequest>> requestEntity = RequestEntity
                .post(uri)
                .header("x-authorization","asdf")
                .header("custom-header","112233")
                .body(req);

        RestTemplate restTemplate = new RestTemplate();

        ResponseEntity<Req<UserResponse>> response = restTemplate
                                                    .exchange(requestEntity, new ParameterizedTypeReference<Req<UserResponse>>() {});

        System.out.println("getStatusCode : "+ response.getStatusCode());
        System.out.println("getHeaders : "+ response.getHeaders());
        System.out.println("getBody : "+ response.getBody());

        return response.getBody();

    }

}

Server

package com.example.server.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Req<T> {
    private Header header;
    private T resBody;

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Header{
        private String responseCode;
    }
}
package com.example.server.controller;

import com.example.server.dto.Req;
import com.example.server.dto.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpEntity;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
@RequestMapping("/api/server")
public class ServerApiController {
    @PostMapping("/user/{userId}/name/{userName}")
    public Req<User> post(
                     @RequestBody Req<User> req,
                     @PathVariable int userId,
                     @PathVariable String userName,
                     @RequestHeader(name ="x-authorization") String authorization ,
                     @RequestHeader(name ="custom-header") String customHeader){
                     
        log.info("userId : {} , userName : {}",userId,userName);
        log.info("x-authorization : {} , custom-header: {}",authorization,customHeader);
        log.info("user : {}",req.getResBody());

        Req<User> response = new Req<>();
        response.setHeader(new Req.Header());
        response.setResBody(req.getResBody());

        return response;
    }
}

'Spring' 카테고리의 다른 글

Jacoco - 테스트 커버리지 확인하기  (1) 2025.04.21
JUnit  (0) 2025.04.21
비동기 처리하기  (1) 2025.04.15
Interceptor  (0) 2025.04.15
서블릿 컨테이너와 Spring 컨텍스트  (0) 2025.04.15

일반적으로 Spring MVC(동기)방식을 권장하지만 필요에 따라 비동기를 설정할 순 있다.

 

 

기본적인 비동기

package com.example.async;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync
public class AsyncApplication {

	public static void main(String[] args) {
		SpringApplication.run(AsyncApplication.class, args);
	}

}

@EnableAsync 어노테이션 추가

 

package com.example.async.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class AsyncService {

    @Async
    public void hello() {

        for(int i=0;i<10;i++){
            try {
                Thread.sleep(2000);
                log.info("thread sleep check");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

@Async aop 기반이기 때문에 프록시 패턴을 따른다. 때문에 public 메소드에만 적용이 가능하다.

package com.example.async.controller;

import com.example.async.service.AsyncService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class ApiController {

    private final AsyncService asyncService;

    @GetMapping("/hello")
    public String hello(){
        asyncService.hello();
        log.info("method end");
        return "hello";
    }
}

@Async 추가를 하게 되면 해당 메소드가 비동기 처리가 되어 쓰레드가 해당 메소드를 실행하고 클라이언트는 쓰레드의 작업이 끝나지 않았으나 응답을 받는다.

 

 

여러 API들의 요청을 조인하여 응답 내려주기 - CompletableFuture

package com.example.async.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;

import java.util.concurrent.CompletableFuture;

@Slf4j
@Service
public class AsyncService {

    private int count =0;

    @Async
    public CompletableFuture run() {

        return new AsyncResult(hello()).completable();
    }



    public String hello() {
        int idx = ++count;
        for(int i=0;i<10;i++){
            try {
                Thread.sleep(2000);
                log.info("thread sleep check {} : {}",idx,i+1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        return "async hello";
    }
}

CompletableFuture 반환하는 함수에 @Async 어노테이션을 추가

@Async로 설정한 메소드 내에서 같은 클래스의 같은 메소드를 호출할 때는 Async를 타지 않는다.

 

package com.example.async.controller;

import com.example.async.service.AsyncService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.CompletableFuture;

@Slf4j
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class ApiController {

    private final AsyncService asyncService;

    @GetMapping("/hello")
    public CompletableFuture hello(){
        log.info("CompletableFuture init");
        CompletableFuture api1 =  asyncService.run();
        CompletableFuture api2 =  asyncService.run();
        CompletableFuture api3 =  asyncService.run();
        return api3;
    }
}

api1, api2, api3 결과가 모두 종료되고 클라이언트에게 결과를 반환한다.

위 내용들은 Spring에서 정한 쓰레드들을 사용하게 된다.

 

 

직접 쓰레드 만들기

package com.example.async.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
public class AppConfig {


    @Bean("async-thread")
    public Executor asyncThread(){
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setMaxPoolSize(100);
        threadPoolTaskExecutor.setCorePoolSize(10);
        threadPoolTaskExecutor.setQueueCapacity(10);
        threadPoolTaskExecutor.setThreadNamePrefix("Async-");
        return threadPoolTaskExecutor;
    }
}

쓰레드 풀을 지정하는 것은 환경, 리퀘스트 양에 따라 달라진다. 때문에 이 풀이 어떻게 동작하는지 정확하게 알고 있어야 설정을 잘할 수 있다.

 

package com.example.async.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;

import java.util.concurrent.CompletableFuture;

@Slf4j
@Service
public class AsyncService {

    @Async("async-thread")
    public CompletableFuture run() {

        return new AsyncResult(hello()).completable();
    }



    public String hello() {
        for(int i=0;i<10;i++){
            try {
                Thread.sleep(2000);
                log.info("thread sleep check ");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        return "async hello";
    }
}

@Async 어노테이션 인자에 Bean으로 설정한 쓰레드 풀의 이름을 설정하게 되면 해당 메소드를 사용할 때 지정한 쓰레드 풀을 사용한다.

  • 몽고DB, Nosql : Spring Webflux 사용 권장
  • rdb : 사실상 async가 의미가 없음 , 데이터베이스 단에서 트랜잭션 떄문에 동기 방식으로 동작하기 때문에 async를 적용시켜도 전체적인 플로우 자체가 async로 동작할 수 없음

'Spring' 카테고리의 다른 글

JUnit  (0) 2025.04.21
Server to Server - RestTemplate  (1) 2025.04.16
Interceptor  (0) 2025.04.15
서블릿 컨테이너와 Spring 컨텍스트  (0) 2025.04.15
Filter  (1) 2025.04.15

+ Recent posts