Service 메서드를 처음 여는 사람은 두 가지를 알고 싶어 한다. 이 메서드가 무엇을 전제하고 동작하는지, 그리고 어디까지 왔을 때 무엇이 보장되는지. 보통은 주석이 그 역할을 한다.
// dto는 컨트롤러에서 이미 검증됨
// 여기서 재고는 확보된 상태
주석의 문제는 코드가 바뀌어도 그대로 남는다는 것이다. 검증 로직이 사라져도, 재고 확보 순서가 바뀌어도 주석은 여전히 옛말을 하고 있다. 읽는 사람은 그 주석을 믿을지 말지부터 판단해야 한다.
assert는 같은 말을 검증되는 형태로 적는 방법이다. 그리고 여기에는 두 가지 목적이 있다.
첫째, 흐름을 빠르게 파악하게 한다. 메서드 상단의 사전조건은 “이 코드가 무엇을 믿고 동작하는지”를, 중간의 불변조건은 “여기까지 왔다면 무엇이 참인지”를 선언한다. 읽는 사람이 로직을 거슬러 올라가며 전제를 재구성하지 않아도 된다.
둘째, 문제를 숨기지 않는다. 전제가 깨졌는데도 그럴듯하게 넘어가는 코드는 버그를 데이터로 만든다. assert는 “이건 있을 수 없다”를 선언하고, 실제로 일어나면 그 자리에서 멈춘다. 오류 지점과 증상 지점의 거리를 좁히는 것이 디버깅 시간을 줄이는 가장 확실한 방법이다.
이 글은 Assert로 프로그래머의 실수를 잡아내기에서 정리한 “검증 계층 통과 후는 assert 영역”이라는 원칙을, 계층별 DTO 네이밍 표준의 Command → Service 구간에 구체적으로 적용한다.
원칙 1. Service의 계약은 “값 검증은 끝났다”이다
Service 메서드가 CreateOrderCommand를 받았을 때 무엇을 가정할 수 있는가. 이걸 먼저 못 박아야 나머지가 정해진다.
이 표준에서 Service의 계약은 이렇다 — 들어온 값은 이미 형식 검증을 통과했다. Service는 비즈니스 규칙만 판단한다.
그러면 그 계약을 지키는 것은 누구인가. 각 진입점이다.
@Valid"] --> CMD["CreateOrderCommand
순수 데이터 홀더"] C2["Batch Job
자체 값 검증"] --> CMD C3["CLI Adapter
자체 값 검증"] --> CMD CMD --> SVC["OrderService"] SVC -.->|"assert · 개발/테스트에서만"| A["계약 위반 감지
AssertionError"] SVC --> BIZ["비즈니스 규칙 검증
BusinessException"] style CMD fill:#2d3748,stroke:#4299e1,stroke-width:2px,color:#e2e8f0 style SVC fill:#1a202c,stroke:#48bb78,stroke-width:3px,color:#e2e8f0 style A fill:#2d3748,stroke:#ed8936,stroke-width:2px,color:#e2e8f0 style BIZ fill:#2d3748,stroke:#f56565,stroke-width:2px,color:#e2e8f0
HTTP로 들어오면 계층별 검증 표준대로 @Valid가 처리한다. 배치나 CLI처럼 HTTP가 아닌 경로로 들어온다면, 그 진입점이 자기 방식으로 값 검증을 마친 뒤 Command를 만들어야 한다. 어느 경로든 Service에 도달할 때는 값 검증이 끝나 있다는 것이 계약이다.
Command는 순수 데이터 홀더다
그래서 Command는 생성자에서 검증하지 않는다.
// ✅ 검증 없이 값만 옮긴다
@Getter
public class CreateOrderCommand {
private final String productName;
private final Integer quantity;
private final String couponCodeOrNull;
private CreateOrderCommand(String productName, Integer quantity, String couponCodeOrNull) {
this.productName = productName;
this.quantity = quantity;
this.couponCodeOrNull = couponCodeOrNull;
}
public static CreateOrderCommand of(String productName, Integer quantity, String couponCodeOrNull) {
return new CreateOrderCommand(productName, quantity, couponCodeOrNull);
}
}
Null-free 객체 설계의 “생성자가 완전성을 보장한다”와 어긋나 보이지만, 이유가 있다. 검증을 여러 곳에 두면 어디가 진짜 방어선인지 흐려진다. Command가 자체 검증을 하면 진입점 검증이 느슨해져도 티가 안 나고, 결국 “어디선가는 막히겠지”라는 상태가 된다.
방어선을 경계 한 곳으로 몰아두면 책임이 분명해진다. 그리고 그 방어선이 뚫렸는지는 — 다음 원칙이 감시한다.
원칙 2. 사전조건 assert로 계약을 명시한다
메서드 첫머리에 “내가 무엇을 가정하고 동작하는지”를 적는다.
@Service
@RequiredArgsConstructor
public class OrderService {
private final StockRepository stockRepository;
private final OrderRepository orderRepository;
public ServiceResult<OrderDomain> createOrder(CreateOrderCommand command) {
assert command.getProductName() != null && !command.getProductName().isBlank()
: "productName은 진입점에서 검증되어야 한다";
assert command.getQuantity() != null && command.getQuantity() > 0
: "quantity는 진입점에서 1 이상으로 검증되어야 한다";
StockDomain stock = stockRepository.findByProductName(command.getProductName())
.orElseThrow(() -> new BusinessException("등록되지 않은 상품입니다."));
stock.validateOrderable(command.getQuantity());
// ...
}
}
이 assert들이 하는 일은 사용자 입력 검증이 아니다. 그 값들은 이미 검증됐다. 여기서 잡히는 것은 진입점이 계약을 어겼다는 사실 — 즉 프로그래머의 실수다. 배치를 새로 만들면서 값 검증을 빠뜨렸다면, 운영에 나가기 전 테스트에서 걸린다.
로직이 실제로 의존하는 것만 적는다
전제를 전부 나열하면 노이즈가 된다. 기준은 하나다 — 이 메서드의 로직이 그 전제에 실제로 기대고 있는가.
// ❌ 이 메서드는 couponCode를 쓰지도 않는다
assert command.getCouponCodeOrNull() == null || !command.getCouponCodeOrNull().isBlank();
// ❌ 아무 정보도 주지 않는다
assert command != null;
command != null은 적어봐야 읽는 사람에게 알려주는 게 없다. 바로 다음 줄에서 역참조하니 NPE로 즉시 드러나기 때문이다. assert의 가치는 “이 조건이 깨진 채로도 코드가 한참 굴러가는” 경우에 있다.
원칙 3. 사후조건 assert로 결과를 보장한다
사전조건보다 실질적인 버그를 더 많이 잡는 쪽이다. 계산이나 상태 전이가 끝난 뒤, 결과가 만족해야 할 조건을 적는다.
public ServiceResult<OrderDomain> applyDiscount(ApplyDiscountCommand command) {
BigDecimal originalPrice = order.getTotalPrice();
BigDecimal finalPrice = discountPolicy.apply(originalPrice, command.getCouponCodeOrNull());
assert finalPrice.compareTo(originalPrice) <= 0
: "할인 후 금액이 원래 금액보다 클 수 없다";
assert finalPrice.compareTo(BigDecimal.ZERO) >= 0
: "할인 후 금액은 음수일 수 없다";
return ServiceResult.of(order.changePrice(finalPrice));
}
할인 정책에 새 쿠폰 타입을 추가하다 부호를 잘못 쓰면, 이 assert가 정책 클래스가 아니라 그것을 쓰는 지점에서 즉시 잡아낸다. 이런 종류의 버그는 assert가 없으면 한참 뒤 결제 금액이 이상하다는 제보로 발견된다.
흐름을 나누는 표지가 된다
사전조건과 사후조건을 같이 쓰면 메서드가 자연스럽게 구간으로 나뉜다.
public ServiceResult<OrderDomain> confirmOrder(ConfirmOrderCommand command) {
assert command.getOrderId() != null : "orderId는 진입점에서 검증되어야 한다";
OrderDomain order = orderRepository.findById(command.getOrderId())
.orElseThrow(() -> new BusinessException("존재하지 않는 주문입니다."));
order.validateConfirmable();
assert order.isPending() : "confirmable 검증을 통과했다면 PENDING 상태여야 한다";
OrderDomain confirmed = order.confirm();
assert confirmed.isConfirmed() : "confirm() 이후에는 CONFIRMED 상태여야 한다";
return ServiceResult.of(orderRepository.save(confirmed));
}
가운데 assert가 “여기까지 왔다면 이건 참이다”를 선언한다. 새로 합류한 사람이 이 메서드를 읽을 때, 각 구간이 무엇을 전제하고 무엇을 만들어내는지 주석 없이 파악할 수 있다. 그리고 이 설명은 테스트가 돌 때마다 사실인지 확인된다.
원칙 4. 잘못된 상태를 조용히 보정하지 않는다
assert의 반대편에는 문제를 발견하고도 그럴듯하게 넘어가는 코드가 있다.
// ❌ 잘못된 값을 조용히 고친다
int quantity = command.getQuantity() <= 0 ? 1 : command.getQuantity();
// ❌ 예외를 삼키고 기본값으로 계속 간다
BigDecimal price;
try {
price = pricingClient.fetch(productId);
} catch (Exception e) {
log.warn("가격 조회 실패", e);
price = BigDecimal.ZERO;
}
두 코드 모두 그 순간에는 아무 일도 일어나지 않는다. 대신 잘못된 값이 정상인 척 DB에 저장되고, 몇 주 뒤 “정산이 안 맞는다”는 제보로 발견된다. 그때는 어느 지점에서 틀어졌는지 추적하기가 훨씬 어렵다. 로그를 남겼더라도 마찬가지다 — 아무도 안 보는 warn 로그는 문제를 숨긴 것과 같다.
quantity <= 0은 진입점에서 이미 걸렀어야 하는 값이다. 여기까지 왔다는 건 계약이 깨졌다는 뜻이므로, 보정할 게 아니라 드러내야 한다.
// ✅ 계약 위반이면 멈춘다
assert command.getQuantity() > 0 : "quantity는 진입점에서 1 이상으로 검증되어야 한다";
int quantity = command.getQuantity();
가격 조회 실패처럼 운영에서도 실제로 일어날 수 있는 일은 assert의 영역이 아니다. 이건 예외로 다룬다. 다만 기준은 똑같다 — 조용히 넘어가지 않는 것.
// ✅ 실패를 드러낸다. 삼키지 않으면 예외가 그대로 올라간다
BigDecimal price = pricingClient.fetch(productId);
기본값으로 대체해야 하는 비즈니스적 이유가 실제로 있다면 그건 보정이 아니라 정책이므로, 그 사실이 코드에 드러나야 한다.
// ✅ 기본값이 정책이라면 이름으로 밝힌다
BigDecimal price = pricingPolicy.fetchOrFallbackToListPrice(productId);
규칙은 하나다. 있을 수 없는 일은 assert로, 일어날 수 있는 실패는 예외로 드러낸다. 어느 쪽이든 숨기지 않는다.
원칙 5. assert를 쓰면 안 되는 곳
assert는 운영에서 꺼진다. 이 사실 하나에서 금지 규칙이 전부 파생된다.
외부 입력 검증에 쓰지 않는다
// ❌ 운영에서는 이 줄이 사라진다 — 무방비
assert email.matches(EMAIL_PATTERN);
이건 계층별 검증 표준의 영역이다. “이게 없으면 잘못된 데이터가 통과하는” 검증은 절대 assert로 쓰지 않는다.
부수 효과가 있는 식을 넣지 않는다
// ❌ 개발과 운영의 동작이 달라진다
assert orderRepository.updateStatus(id) > 0;
assert 안의 식은 읽기만 해야 한다. 상태를 바꾸면 -ea 여부에 따라 프로그램이 다르게 동작하고, 이런 버그는 재현조차 어렵다.
도달 불가 분기는 AssertionError를 직접 던진다
// ❌ 운영에서 꺼지면 아무 일 없이 통과한다
default -> { assert false : "처리되지 않은 상태"; }
// ✅ -ea와 무관하게 항상 동작한다
default -> throw new AssertionError("처리되지 않은 주문 상태: " + status);
assert 문과 AssertionError를 던지는 것은 다르다. “여기 도달하면 그 자체로 버그”인 지점은 운영에서도 막아야 하므로 직접 던진다.
이전 상태를 참조하는 사후조건은 비용을 확인한다
// 이 줄은 운영에서도 실행된다 (assert 밖이므로)
int before = order.getQuantity();
processOrder(order);
assert order.getQuantity() == before - 1;
Java의 assert는 “이전 값”을 자동으로 잡아주지 않아서 미리 변수에 담아야 하는데, 그 변수를 만드는 코드는 assert가 아니므로 운영에서도 실행된다. 무거운 연산이라면 사후조건을 포기하는 편이 낫다.
원칙 6. -ea는 테스트와 로컬 개발에서만 켠다
assert가 실제로 실행되지 않으면 이 표준 전체가 무의미하다. 다행히 테스트는 기본으로 켜져 있다. Gradle의 Test 태스크와 Maven Surefire 모두 enableAssertions 기본값이 true다.
로컬 실행에는 명시해야 한다.
tasks.named('bootRun') {
jvmArgs('-ea')
}
운영에서는 켜지 않는다. 검증이 필요한 것은 이미 예외로 막았고, assert는 개발 단계에서 계약 위반을 잡는 장치이기 때문이다. -ea 옵션의 자세한 동작은 기존 assert 글에 정리해두었다.
판단 기준 정리
| 상황 | 수단 | 이유 |
|---|---|---|
| 사용자 입력 형식 검증 | Bean Validation (진입점) | 운영에서도 반드시 동작해야 함 |
| 비즈니스 규칙 위반 | BusinessException |
정상적으로 발생 가능한 실패 |
| 진입점이 계약을 지켰는지 | assert (사전조건) |
프로그래머의 실수 감지 |
| 계산·상태 전이 결과 보장 | assert (사후조건) |
프로그래머의 실수 감지 |
| 복잡한 분기 후 상태 선언 | assert (중간 불변조건) |
흐름을 나누는 표지 |
| 도달하면 안 되는 분기 | throw new AssertionError |
운영에서도 막아야 함 |
| 부수 효과가 있는 검사 | assert 금지 | 환경에 따라 동작이 달라짐 |
잘못된 값 보정 (<= 0이면 1로) |
금지 — assert로 드러냄 | 버그가 데이터로 남는다 |
| 예외 삼키고 기본값 사용 | 금지 — 예외를 올림 | 실패가 조용히 묻힌다 |
| 기본값 대체가 정책일 때 | 메서드 이름으로 밝힘 | 보정과 정책은 다르다 |
이 표준을 정하기까지
출발점은 “Command가 들어온 시점에 무엇을 가정할 수 있는가”였다. 기존 assert 글에서 “검증 계층 통과 후는 assert 영역”이라고 정리해뒀으니, Request → Command → Service 구조에서는 Service 진입점이 정확히 그 자리다. 여기서 assert를 쓰면 전제를 코드로 남길 수 있겠다는 생각에서 시작했다.
그런데 assert가 운영에서 꺼진다는 사실이 범위를 강하게 제한했다. “이게 없으면 잘못된 데이터가 통과하는” 검증에는 쓸 수 없다는 뜻이고, 뒤집으면 이미 다른 곳에서 보장된 것을 재확인할 때만 쓸 수 있다. 중복처럼 보이지만 그게 정확히 assert의 용도다 — 사용자 입력이 아니라 파이프라인이 깨졌는지를 잡는 것.
문제는 진입점이 여러 개일 때였다. HTTP는 @Valid가 막아주지만 배치나 CLI는 그 경로를 안 거친다. 그 경로에서 값 검증이 빠지면 Service는 무방비가 되는데, assert는 운영에서 꺼져 있으니 잡아주지도 못한다.
그래서 Service의 계약을 먼저 정의했다. “들어온 값은 이미 형식 검증을 통과했다”를 계약으로 못 박고, 그 계약을 지키는 책임을 각 진입점에 두었다. HTTP면 @Valid, 배치면 배치가, CLI면 CLI가 알아서 값 검증을 마치고 Command를 만든다. 계약이 명확해지니 assert의 역할도 분명해졌다 — 계약이 지켜졌는지를 개발·테스트 단계에서 감시하는 장치.
Command를 순수 데이터 홀더로 둔 것도 같은 이유다. Command가 자체 검증을 하면 방어선이 두 겹이 되는데, 그러면 진입점 검증이 느슨해져도 티가 안 난다. 결국 “어디선가는 막히겠지” 상태가 되고, 진짜 방어선이 어디인지 아무도 모르게 된다. 방어선을 경계 한 곳으로 몰고, 그게 뚫렸는지는 assert가 감시하는 구조가 더 명확했다.
사전조건과 사후조건을 둘 다 쓰기로 한 건 흐름 문서화 때문이다. 실제로 버그를 더 많이 잡는 쪽은 사후조건이지만, 사전조건이 있어야 “이 메서드가 무엇을 전제하는지”가 메서드 상단에서 드러난다. 둘을 같이 쓰면 메서드가 구간으로 나뉘고, 각 구간의 전제와 결과가 눈에 보인다.
정리하다 보니 이 표준의 진짜 목적이 계약 검증이 아니라는 걸 알게 됐다. 계약은 수단이고, 실제로 얻으려는 건 두 가지였다 — 메서드를 처음 보는 사람이 전제와 보장을 빠르게 파악하는 것, 그리고 문제가 생겼을 때 숨기지 않고 즉시 드러내는 것. 그렇게 보니 assert의 반대편에 무엇이 있는지도 분명해졌다. quantity가 0이면 1로 고쳐 넘어가거나, 예외를 삼키고 기본값으로 계속 가는 코드다. 그 코드들은 그 순간엔 조용하지만 잘못된 값을 정상인 척 DB에 남기고, 몇 주 뒤 정산이 안 맞는다는 제보로 돌아온다. 그래서 “조용히 보정하지 않는다”를 별도 원칙으로 세웠다.
마지막으로 남용을 막을 기준이 필요했다. 전제를 전부 적으면 진짜 중요한 것이 묻히기 때문이다. assert command != null 같은 건 바로 다음 줄에서 NPE로 드러나므로 적을 이유가 없다. 정리해보니 기준은 “이 조건이 깨진 채로도 코드가 한참 굴러가는가”였다. 즉시 드러날 것은 적지 않고, 조용히 잘못된 결과를 만들어낼 것만 적는다.
정리
- assert의 목적은 두 가지다. 흐름을 빠르게 파악하게 하는 것, 그리고 문제를 숨기지 않는 것
- Service의 계약은 “값 검증은 끝났다”이다. 그 계약을 지키는 건 각 진입점(Controller, 배치, CLI)의 책임이다
- Command는 순수 데이터 홀더다. 방어선을 경계 한 곳에 두어야 책임이 흐려지지 않는다
- 사전조건 assert는 계약을 명시한다. 잡히는 것은 사용자 입력 오류가 아니라 진입점의 계약 위반이다
- 사후조건 assert는 계산 결과를 보장한다. 실제 버그는 이쪽에서 더 많이 잡힌다
- 적는 기준은 “깨진 채로도 코드가 한참 굴러가는가”이다. 즉시 드러날 것은 적지 않는다
- 잘못된 상태를 조용히 보정하지 않는다. 있을 수 없는 일은 assert로, 일어날 수 있는 실패는 예외로 드러낸다
- 외부 입력 검증과 부수 효과에는 쓰지 않는다. 운영에서 꺼지기 때문이다
- 도달 불가 분기는
AssertionError를 직접 던진다. 운영에서도 막아야 한다 -ea는 테스트와 로컬에서만 켠다. 테스트는 기본값이 이미 켜져 있다
AI 코드 어시스턴트에 바로 적용하기
Claude Code — .claude/skills/service-assert-standard/SKILL.md
---
name: service-assert-standard
description: Service 계층에서 assert를 사용해 계약을 명시하는 규칙. Service 메서드, Command 처리, 사전조건/사후조건을 작성하거나 리뷰할 때 적용한다.
---
# Service 계층 assert 표준
assert의 목적은 두 가지다. **메서드의 흐름을 빠르게 파악하게 하는 것**, 그리고 **문제를 숨기지 않고 즉시 드러내는 것**이다.
전제로, Service의 계약은 **들어온 값은 이미 형식 검증을 통과했다**이다. 계약을 지키는 책임은 각 진입점(Controller, 배치, CLI)에 있고, assert는 그 계약이 지켜졌는지를 개발/테스트 단계에서 감시한다.
## Command
- `Command`와 `Query`는 순수 데이터 홀더로 만든다. 생성자에서 검증하지 않는다.
- 방어선은 진입점 한 곳에만 둔다. Command가 자체 검증하면 진짜 방어선이 어디인지 흐려진다.
## 사전조건 assert
- Service 메서드 첫머리에 그 메서드가 가정하는 조건을 `assert`로 적는다.
- 메시지에는 "누가 보장해야 하는가"를 적는다. 예: `assert command.getQuantity() > 0 : "quantity는 진입점에서 1 이상으로 검증되어야 한다"`.
- 메서드 로직이 실제로 의존하는 전제만 적는다. 사용하지 않는 필드는 적지 않는다.
- `assert command != null`처럼 바로 다음 줄에서 NPE로 드러나는 것은 적지 않는다.
## 사후조건 assert
- 계산이나 상태 전이 후 결과가 만족해야 할 조건을 `assert`로 적는다.
- 복잡한 분기 뒤에는 "여기 도달했다면 X는 참이다"를 중간 불변조건으로 적는다.
- 이전 상태를 변수에 담아 비교하는 사후조건은, 그 변수를 만드는 코드가 운영에서도 실행됨을 고려한다. 연산이 무거우면 생략한다.
## 잘못된 상태를 숨기지 않는다
- 잘못된 값을 조용히 보정하지 않는다. `quantity <= 0`이면 1로 바꾸는 식의 코드를 만들지 말고, `assert`로 계약 위반을 드러낸다.
- 예외를 삼키고 기본값으로 계속 진행하지 않는다. `catch` 후 로그만 남기고 기본값을 대입하는 코드를 만들지 않는다.
- 기본값 대체가 실제 비즈니스 정책이라면, 그 사실이 드러나는 메서드명으로 표현한다. 예: `fetchOrFallbackToListPrice()`.
- 판단 기준: 있을 수 없는 일은 `assert`로, 운영에서 일어날 수 있는 실패는 예외로 드러낸다. 어느 쪽이든 숨기지 않는다.
## 금지
- 외부 입력 검증에 `assert`를 쓰지 않는다. assert는 운영에서 비활성화되므로 무방비가 된다.
- `assert` 식 안에 상태를 변경하는 코드를 넣지 않는다. 읽기 전용이어야 한다.
- 도달 불가 분기에는 `assert false`가 아니라 `throw new AssertionError(...)`를 쓴다. 운영에서도 막아야 하기 때문이다.
- 비즈니스 규칙 위반은 `assert`가 아니라 `BusinessException`으로 던진다.
## 실행 설정
- 테스트는 Gradle `Test` 태스크와 Maven Surefire 모두 `enableAssertions` 기본값이 `true`이므로 별도 설정이 필요 없다.
- 로컬 실행은 `bootRun`에 `jvmArgs('-ea')`를 추가한다.
- 운영에서는 `-ea`를 켜지 않는다.
GitHub Copilot — .github/instructions/service-assert-standard.instructions.md
---
description: Service 계층 assert 사용 규칙
applyTo: "**/*.java"
---
- `Command`와 `Query`는 순수 데이터 홀더로 만들고 생성자에서 검증하지 않는다.
- Service 메서드 첫머리에 사전조건을 `assert`로 적고, 메시지에 누가 보장해야 하는지 명시한다.
- 메서드 로직이 실제로 사용하는 필드에 대해서만 사전조건을 적는다.
- `assert command != null`처럼 즉시 NPE로 드러나는 조건은 적지 않는다.
- 계산이나 상태 전이 후에는 결과가 만족해야 할 조건을 `assert`로 적는다.
- 복잡한 분기 뒤에는 "여기 도달했다면 참인 조건"을 중간 불변조건으로 적는다.
- 잘못된 값을 조용히 보정하지 않는다. `quantity <= 0`이면 1로 바꾸는 식의 코드 대신 `assert`로 드러낸다.
- 예외를 `catch`해서 로그만 남기고 기본값으로 계속 진행하지 않는다.
- 기본값 대체가 비즈니스 정책이라면 `fetchOrFallbackToListPrice()`처럼 메서드명으로 드러낸다.
- 외부 입력 검증에 `assert`를 사용하지 않는다.
- `assert` 식 안에 상태를 변경하는 코드를 넣지 않는다.
- 도달 불가 분기에는 `assert false` 대신 `throw new AssertionError(...)`를 쓴다.
- 비즈니스 규칙 위반은 `assert`가 아니라 `BusinessException`으로 던진다.
- 로컬 실행 설정에는 `-ea` JVM 옵션을 추가하고, 운영에는 추가하지 않는다.
자신만의 철학을 만들어가는 중입니다.
댓글남기기