먼저 조건을 꼼꼼히 읽고 문제가 어떤 풀이를 요구하는지 생각해보았다.
문자열
s와 거리n을 입력받아s를n만큼 민 암호문을 만드는 함수
s는 알파벳 소문자, 대문자, 공백으로만 이루어져 있습니다.
1. "z"는 1만큼 밀면 "a"가 됩니다. → 원형 큐 형태
2. 공백은 아무리 밀어도 공백입니다. → 따로 처리 필요
3. n은 1 이상, 25이하인 자연수입니다. → 알파벳 범위 내에서 순환 → % 26
문제에서 주어진 동작 문자열 밀기를 어떻게 코드 상에서 구현할까 했다.
먼저 ASCII 숫자를 증가시켜 다음 문자로 밀면 되겠다 싶었지만
숫자는 숫자,소문자는 소문자끼리 순환해야한다.
그래서 문자의 각 끝 지점마다 다음으로 밀면 다시 처음의 문자로 돌아가는 처리를 따로 해주고, 나머지는 ASCII증가로 처리해보기로 했다.
class Solution {
public String solution(String s, int n) {
String answer = "";
for(char c : s.toCharArray()){
if(c == 'z' || c == 'Z'){}
}
return answer;
}
}
하지만 이러면 예를 들어 n이 3 이상이라면,
z 이전의 x, y 도 한 바퀴를 돌아야 하지만 처리가 되지 않는다.
class Solution {
public String solution(String s, int n) {
StringBuilder sb = new StringBuilder();
for(char c : s.toCharArray()){
if(c == ' ') { sb.append(c); continue;}
int C = c + n; // 변경 후 문자
if ((c <= 'Z' && C > 'Z') ||
(c >= 'a' && C > 'z')) {
sb.append((char)(C - 26));
}
else {
sb.append((char)(c + n));
}
}
return sb.toString();
}
}
% 연산을 사용하면 알파벳 울타리(26개)를 벗어났는지 일일이 검사할 필요 없이, 모든 문자를 하나의 수식으로 처리할 수 있다.
기준점(base)을 0으로 맞춘 뒤 n만큼 더하고 다시 26으로 나눈 나머지를 구하면, 범위를 넘어가도 자동으로 처음(a 또는 A)으로 돌아온다.
class Solution {
public String solution(String s, int n) {
StringBuilder sb = new StringBuilder();
for (char c : s.toCharArray()) {
if (c == ' ') {
sb.append(c);
continue;
}
// 기준점 - 대문자 : A, 소문자 : a;
char base = (c <= 'Z') ? 'A' : 'a';
char shifted = (char) ((c - base + n) % 26 + base);
sb.append(shifted);
}
return sb.toString();
}
}
목적: 멀티쓰레드 환경에서 로그의 문맥(Context)을 유지하여 추적성을 확보한다.
로그 출력 시 어떤 요청에서 발생한 일인지 식별하기 위해 주로 다음 정보를 담는다.
- traceId: 요청당 부여되는 고유 식별값
- requestUri: 요청이 들어온 엔드포인트
- httpMethod: 호출 방식 (GET, POST 등)
MDC는 내부적으로 ThreadLocal을 사용한다. 따라서 비동기(@Async) 처리를 할 경우 쓰레드가 바뀌면서 기존 MDC 정보가 소실되는데,
이를 해결하기 위해 Task Decorator를 사용한다.
부모 쓰레드의 MDC를 자식 쓰레드로 복사해줌으로써 로그의 연속성을 보장한다.
| 구분 | 자동 빈 등록 (Component Scan) | 수동 빈 등록 (@Configuration) |
|---|---|---|
| 방식 | @Component 등 어노테이션 사용 | @Bean 직접 정의 |
| 특징 | 편리하고 빠름 | 의존성 관계가 명확하고 선택적 로드 가능 |
[핵심 논리]
공통 모듈의 모든 기능을 자동 등록하면 사용하지 않는 서비스에서도 불필요한 빈이 생성된다. 기존 서비스의 의존성을 침범하지 않도록, 공통 모듈은 인터페이스와 구현체만 제공하고 사용하는 서비스에서 필요한 것만 수동 등록하여 주입하는 방식이 더 안전하다.
MSA 설계의 핵심 경계인 Bounded Context를 기준으로 서버를 나눈다.
보통 하나의 Bounded Context당 하나의 마이크로서비스를 할당하여 시스템 간의 결합도를 최소화한다.
일관성 유지를 위해 바운디드 컨텍스트는 하나의 애그리거트 루트로만 접근한다.

비즈니스의 핵심 규칙을 정의하는 '레시피' 단계로, 외부 기술에 의존하지 않는 순수한 자바 객체들로 구성된다.
모델링 도구:
1. 루트 애그리거트 (Entity)정의: 해당 도메인에서 보호해야 할 비즈니스 규칙을 정리한다. 도메인 계층의 도메인 로직은 할 일만 알려주는 '레시피' 역할만 하며, 실제 실행은 서비스 계층이 담당한다.
2. Aggregate에 사용되는 VO 구성: VO로 각자의 데이터와 로직을 캡슐화한다. 엔티티의 Id도 VO가 될수도 있다. VO 안에도 VO가 포함될 수 있다.
정적 팩토리 메서드(of, from)를 통해 유효한 객체 생성을 강제한다.
VO는 불변 객체여야 하며, 생성 시점에 모든 값이 유효해야 한다.
이를 위해 생성자를 직접 노출하기보다 private으로 감추고,
의미 있는 이름을 가진 정적 팩토리 메서드를 통해 객체를 생성하도록 강제한다.
of, from, createDefault)VO는 독립적인 식별자가 없으므로, 스스로 존재할 수 없다. VO의 생명주기는 해당 VO를 포함하고 있는 애그리거트 루트(Aggregate Root)의 생명주기와 완전히 일치해야 한다.
3. 도메인 서비스: User 관점에서 다른 서비스의 개념을 객체로 생성한다.
다른 서비스의 정보가 필요하거나, 특정 엔티티에 귀속시키기 모호한 비즈니스 로직을 정의한다. (정보를 가져오는 방식은 FeignClient나 Event 처리 등 마음대로 한다.)
4. 도메인 이벤트: 상태 변화를 외부에 알릴 이벤트 객체를 정의한다.(예:
OrderCreated)
도메인에서 정의한 규칙들이 실제 세상(DB, 메시징 시스템 등)과 만나는 지점이다.
두 서비스는 모두 '로직'을 담고 있지만, 그 성격과 책임이 명확히 다르다.
| 구분 | 도메인 서비스 | 응용 서비스 (Application Service) |
|---|---|---|
| 핵심 역할 | 비즈니스 요구사항의 개념화 및 규칙 정의 | 비즈니스 로직의 실행 및 흐름 제어 |
| 의존성 | 외부 구현 기술이나 프레임워크에 의존하지 않음 | Infrastructure(DB, 메시징 등)와 도메인을 연결 |
| 주요 특징 | 한 애그리거트에 담기 모호한 로직을 처리 | 실제 사용자 요청이 들어올 때 실행됨 |
| 비유 | 요리의 레시피 (재료의 조합과 순서 규칙) | 요리사 (레시피대로 재료를 가져와 요리함) |
1. 도메인 서비스가 필요한 경우
2. 응용 서비스가 필요한 경우
5. 리포지토리(Repository) 구현: 도메인 계층에서 선언한 인터페이스를 JPA 등을 이용해 실제로 구현하고 DB와 연결한다.
CQRS 패턴
Repository - 명령 담당, 상태관리: 추가,수정,삭제
'Query' Repository - 조회
성능을 높이기 위해선 빈도가 많은 조회쪽의 성능을 높여야 한다.
분리해서 쉽게 교체가 이루어지게 함.
예를 들어 추가,수정,삭제는 JPA를 사용하고, 조회에는 MyBatis 사용하여 명령 책임을 분리한다. 분리된 조회 쪽엔 엘라스틱 서치등을 추가할 수도 있다.
5+. CQRS 및 Query 패키지 구성: 명령(CUD)과 조회(R) 책임을 분리한다.
구현체가 무엇이든 상관하지 않고 비즈니스 흐름을 지휘하는 곳이다.
6. 응용 서비스(Application Service) 작성:
도메인 객체들에게 일을 시키고, 트랜잭션 관리와 보안 등 인프라와 도메인을 연결하는 흐름을 작성한다.
7. 이벤트 발행 및 소비 로직:
도메인 계층에서 정의한 이벤트를 실제로 던지거나(발행 Publish) 받는 (구독(Subscribe)) 흐름을 작성한다.
사용자가 실제 요청을 보냈을 때 처리의 흐름을 정의한다.
- DTO 설계 및 Controller 작성:
외부 API 스펙에 맞춘 DTO를 만들고, 요청을 받아 Application 계층으로 넘겨주는 컨트롤러를 구현한다.
데이터 충돌이 적을 것이라 가정하고 JPA의 @Version 등을 활용해 제어하는 방식이다.
낙관락 예외가 발생했을 때 성공할 때 까지 재시도를 해야한다고 생각했는데
대신 사용자에게 다시 시도해야 함을 전달하는 것이 원칙이다.
이벤트 기반 처리에서는 '어떻게든 성공'시키는 것이 중요하지만, 수정 로직에서는 '사용자가 보고 있는 데이터의 시점'이 곧 정합성의 기준이기 때문이다.
여러 서버 환경에서 확실한 자원 격리가 필요할 때는 Redisson 등을 활용해 분산락을 구현한다.