세션8. 시스템 데이터
Last updated
Last updated
시스템 각 구성요소를 설계하는 접근법은 다양하다. 그 중 데이터베이스에서 시작하는 설계와 인터페이스에서 시작하는 설계를 설명하고 TDD 절차와 비교해보자.
데이터베이스 제약 조건을 통해 데이터의 일관성과 무결성이 유지하기가 쉽다.
데이터베이스 스키마는 모델과 코드에 비해 변경 비용이 크기 때문에, 시스템 설계 유연성이 낮아진다.
데이터베이스 스키마는 비지니스 지식 표현력이 낮다.
작업 부하 분산이 어렵다. (아직 왜 그런지는 모르겠다)
응용프로그램의 역할을 클라이언트와 데이터베이스 연산의 연결수단으로 바라본다.
인터페이스 설계가 데이터베이스 스키마에 논리적으로 의존한다.
기능 및 비기능 요구사항 난이도가 낮고 변경이 적을수록 유용하다.
데이터베이스 중심 아키텍처가 대표적
클라이언트에 제공되는 가치가 설계 결정 주도권을 같는다.
비지니스 지식이 시스템에 풍부한 어휘로 표현된다.
인터페이스 설계는 데이터베이스 스키마에 의존하지 않는다.
구현 기술을 다양하게 검토할 수 있다.(아직 왜 그런지 모르겠다)
이전 코드까지는 시스템에 상태가 필요하지 않았기 때문에, DB 를 사용하지 않았다.
하지만 이제부터 아래 요구사항을 충족하기 위해서는 상태 관리를 해야하기 때문에, DB 가 필요하다.
email 속성에 이미 존재하는 이메일 주소가 지정되면 400 Bad Request 상태코드를 반환한다
package commerce;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
public class Seller {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long dataKey;
@Column(unique = true)
private String email;
}
package commerce;
import org.springframework.data.jpa.repository.JpaRepository;
public interface SellerRepository extends JpaRepository<Seller, Long> {
}
package commerce.api.controller;
import commerce.Seller;
import commerce.SellerRepository;
import commerce.command.CreateSellerCommand;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public record SellerSignUpController(SellerRepository repository) {
public static final String EMAIL_REGEX = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}$";
public static final String USERNAME_REGEX = "^[a-zA-Z0-9_-]{3,}$";
@PostMapping("/seller/signUp")
ResponseEntity<?> signUp(@RequestBody CreateSellerCommand command) {
if (isCommandValid(command) == false) {
return ResponseEntity.badRequest().build();
}
var seller = new Seller();
seller.setEmail(command.email());
try {
repository.save(seller);
} catch (DataIntegrityViolationException exception) {
return ResponseEntity.badRequest().build();
}
return ResponseEntity.noContent().build();
}
private static boolean isCommandValid(CreateSellerCommand command) {
return isEmailValid(command.email())
&& isUsernameValid(command.username())
&& isPasswordValid(command.password());
}
private static boolean isEmailValid(String email) {
return email != null && email.matches(EMAIL_REGEX);
}
private static boolean isUsernameValid(String username) {
return username != null && username.matches(USERNAME_REGEX);
}
private static boolean isPasswordValid(String password) {
return password != null && password.length() >= 8;
}
}
package test.commerce.api.seller.signup;
import commerce.CommerceApiApp;
import commerce.command.CreateSellerCommand;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.ResponseEntity;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(
classes = CommerceApiApp.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
@DisplayName("POST /seller/signUp")
public class POST_specs {
...
@Test
void email_속성에_이미_존재하는_이메일_주소가_지정되면_400_Bad_Request_상태코드를_반환한다(
@Autowired TestRestTemplate client
) {
// Arrange
String email = "seller@test.com";
client.postForEntity(
"/seller/signUp",
new CreateSellerCommand(email, "seller", "password"),
Void.class
);
// Act
ResponseEntity<Void> response = client.postForEntity(
"/seller/signUp",
new CreateSellerCommand(email, "seller", "password"),
Void.class
);
// Assert
assertThat(response.getStatusCode().value()).isEqualTo(400);
}
}
위 테스트 코드를 실행시켜보면 테스트는 실패한다. 이유는 다음과 같다.
기존 테스트 코드는 고정값을 사용하는데, 고정값을 사용하면 컨트롤러에서 400을 리턴하기 때문에, 마지막 테스트 코드는 성공할찌라도 이전 테스트코드는 실패한다.
이를 위해서 임의 테스트 데이터를 만드는 객체가 필요하다.
테스트 모음의 각 테스트가 독립적이고 안정적으로 실행되도록 EmailGenerator
, usernameGenerator
를 만들어 테스트마다 임의의 데이터를 생성해 사용하자.
추가적으로 아래 요구사항을 충족하는 코드와 테스트를 추가하자.
username 속성에 이미 존재하는 사용자이름이 지정되면 400 Bad Request 상태코드를 반환한다
package commerce;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
public class Seller {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long dataKey;
@Column(unique = true)
private String email;
@Column(unique = true)
private String username;
}
package commerce.api.controller;
import commerce.Seller;
import commerce.SellerRepository;
import commerce.command.CreateSellerCommand;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public record SellerSignUpController(SellerRepository repository) {
public static final String EMAIL_REGEX = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}$";
public static final String USERNAME_REGEX = "^[a-zA-Z0-9_-]{3,}$";
@PostMapping("/seller/signUp")
ResponseEntity<?> signUp(@RequestBody CreateSellerCommand command) {
if (isCommandValid(command) == false) {
return ResponseEntity.badRequest().build();
}
var seller = new Seller();
seller.setEmail(command.email());
seller.setUsername(command.username());
try {
repository.save(seller);
} catch (DataIntegrityViolationException exception) {
return ResponseEntity.badRequest().build();
}
return ResponseEntity.noContent().build();
}
private static boolean isCommandValid(CreateSellerCommand command) {
return isEmailValid(command.email())
&& isUsernameValid(command.username())
&& isPasswordValid(command.password());
}
private static boolean isEmailValid(String email) {
return email != null && email.matches(EMAIL_REGEX);
}
private static boolean isUsernameValid(String username) {
return username != null && username.matches(USERNAME_REGEX);
}
private static boolean isPasswordValid(String password) {
return password != null && password.length() >= 8;
}
}
package test.commerce;
import java.util.UUID;
public class EmailGenerator {
public static String generateEmail() {
return UUID.randomUUID() + "@test.com";
}
}
package test.commerce;
import java.util.UUID;
public class UsernameGenerator {
public static String generateUsername() {
return "username" + UUID.randomUUID();
}
}
package test.commerce.api.seller.signup;
import commerce.CommerceApiApp;
import commerce.command.CreateSellerCommand;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.ResponseEntity;
import static org.assertj.core.api.Assertions.assertThat;
import static test.commerce.EmailGenerator.generateEmail;
import static test.commerce.UsernameGenerator.generateUsername;
@SpringBootTest(
classes = CommerceApiApp.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
@DisplayName("POST /seller/signUp")
public class POST_specs {
@Test
void 올바르게_요청하면_204_No_Content_상태코드를_반환한다(
@Autowired TestRestTemplate client
) {
// Arrange
var command = new CreateSellerCommand(
generateEmail(),
generateUsername(),
"password"
);
// Act
ResponseEntity<Void> response = client.postForEntity(
"/seller/signUp",
command,
Void.class
);
// Assert
assertThat(response.getStatusCode().value()).isEqualTo(204);
}
@Test
void email_속성이_지정되지_않으면_400_Bad_Request_상태코드를_반환한다(
@Autowired TestRestTemplate client
) {
// Arrange
var command = new CreateSellerCommand(
null,
generateUsername(),
"password"
);
// Act
ResponseEntity<Void> response = client.postForEntity(
"/seller/signUp",
command,
Void.class
);
// Assert
assertThat(response.getStatusCode().value()).isEqualTo(400);
}
@ParameterizedTest
@ValueSource(strings = {
"invalid-email",
"invalid-email@",
"invalid-email@test",
"invalid-email@test.",
"invalid-email@.com"
})
void email_속성이_올바른_형식을_따르지_않으면_400_Bad_Request_상태코드를_반환한다(
String email,
@Autowired TestRestTemplate client
) {
// Arrange
var command = new CreateSellerCommand(
email,
generateUsername(),
"password"
);
// Act
ResponseEntity<Void> response = client.postForEntity(
"/seller/signUp",
command,
Void.class
);
// Assert
assertThat(response.getStatusCode().value()).isEqualTo(400);
}
@Test
void username_속성이_지정되지_않으면_400_Bad_Request_상태코드를_반환한다(
@Autowired TestRestTemplate client
) {
// Arrange
var command = new CreateSellerCommand(
generateEmail(),
null,
"password"
);
// Act
ResponseEntity<Void> response = client.postForEntity(
"/seller/signUp",
command,
Void.class
);
// Assert
assertThat(response.getStatusCode().value()).isEqualTo(400);
}
@ParameterizedTest
@ValueSource(strings = {
"",
"se",
"seller ",
"seller.",
"seller!",
"seller@"
})
void username_속성이_올바른_형식을_따르지_않으면_400_Bad_Request_상태코드를_반환한다(
String username,
@Autowired TestRestTemplate client
) {
// Arrange
var command = new CreateSellerCommand(
generateEmail(),
username,
"password"
);
// Act
ResponseEntity<Void> response = client.postForEntity(
"/seller/signUp",
command,
Void.class
);
// Assert
assertThat(response.getStatusCode().value()).isEqualTo(400);
}
@ParameterizedTest
@ValueSource(strings = {
"seller",
"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
"0123456789",
"seller_",
"seller-"
})
void username_속성이_올바른_형식을_따르면_204_No_Content_상태코드를_반환한다(
String username,
@Autowired TestRestTemplate client
) {
// Arrange
var command = new CreateSellerCommand(
generateEmail(),
username,
"password"
);
// Act
ResponseEntity<Void> response = client.postForEntity(
"/seller/signUp",
command,
Void.class
);
// Assert
assertThat(response.getStatusCode().value()).isEqualTo(204);
}
@Test
void password_속성이_지정되지_않으면_400_Bad_Request_상태코드를_반환한다(
@Autowired TestRestTemplate client
) {
// Arrange
var command = new CreateSellerCommand(
generateEmail(),
generateUsername(),
null
);
// Act
ResponseEntity<Void> response = client.postForEntity(
"/seller/signUp",
command,
Void.class
);
// Assert
assertThat(response.getStatusCode().value()).isEqualTo(400);
}
@ParameterizedTest
@ValueSource(strings = {
"",
"pass",
"pass123"
})
void password_속성이_올바른_형식을_따르지_않으면_400_Bad_Request_상태코드를_반환한다(
String password,
@Autowired TestRestTemplate client
) {
// Arrange
var command = new CreateSellerCommand(
generateEmail(),
generateUsername(),
password
);
// Act
ResponseEntity<Void> response = client.postForEntity(
"/seller/signUp",
command,
Void.class
);
// Assert
assertThat(response.getStatusCode().value()).isEqualTo(400);
}
@Test
void email_속성에_이미_존재하는_이메일_주소가_지정되면_400_Bad_Request_상태코드를_반환한다(
@Autowired TestRestTemplate client
) {
// Arrange
String email = generateEmail();
client.postForEntity(
"/seller/signUp",
new CreateSellerCommand(email, generateUsername(), "password"),
Void.class
);
// Act
ResponseEntity<Void> response = client.postForEntity(
"/seller/signUp",
new CreateSellerCommand(email, generateUsername(), "password"),
Void.class
);
// Assert
assertThat(response.getStatusCode().value()).isEqualTo(400);
}
@Test
void username_속성에_이미_존재하는_사용자이름이_지정되면_400_Bad_Request_상태코드를_반환한다(
@Autowired TestRestTemplate client
) {
// Arrange
String username = generateUsername();
client.postForEntity(
"/seller/signUp",
new CreateSellerCommand(generateEmail(), username, "password"),
Void.class
);
// Act
ResponseEntity<Void> response = client.postForEntity(
"/seller/signUp",
new CreateSellerCommand(generateEmail(), username, "password"),
Void.class
);
// Assert
assertThat(response.getStatusCode().value()).isEqualTo(400);
}
}