처음에든 생각은
그런데 Java에서 Python의 '문자'*횟수 문법이 있는지 확실치 않았고, 문자열 더하기를 반복하면 시간이 오래 걸리기 때문에 StringBuilder를 사용해야한다는 생각을 했다.
그래서 처음엔 이렇게 4자리 수를 저장한 StringBuilder를 먼저 만들고 insert() 를 사용했는데, 이는 문자를 뒤로 밀어내는 연산이 필요해서 O(n)이 걸린다고 했다.
class Solution {
public String solution(String phone_number) {
int len = phone_number.length();
StringBuilder sb = new StringBuilder(phone_number.substring(len-4));
for(int i = 0; i < len-4; i++){
sb.insert(0, '*');
}
return sb.toString();
}
}
그래서 순서대로 append()를 하는 방식으로 수정했다.
class Solution {
public String solution(String phone_number) {
int len = phone_number.length();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < len - 4; i++) {
sb.append('*');
}
sb.append(phone_number.substring(len - 4));
return sb.toString();
}
}
결과 테스트 코드의 최악 실행 시간이 0.07ms에서 0.05ms 로 바뀌었다.
앞으로도 StringBuilder를 사용할 땐 문자열을 앞에서부터 완성해 나가는것이 효율적임을 잘 기억해야겠다.
java 11 부터는 다음 방식도 가능하다.
"*".repeat(len - 4)
class Solution {
private int[] count = new int[10]; // 0~9
public int solution(int[] numbers) {
for(int i = 0; i < numbers.length; i++){
int num = numbers[i];
count[num]++;
}
int answer = 0;
for(int num = 0; num < 10; num++){
int cnt = count[num];
if (cnt == 0) answer += num;
}
return answer;
}
}
생각해보니 등장 했는지/아닌지 이기 때문에 boolean[]을 사용해도 된다.
다른 사람 코드를 보니 에서 나타난 수를 빼는 방법도 있었다. 이런 생각은 어떻게 하는걸까? 신기하다.
기능 개발에 대한 지식을 학습하는 것보다 시스템을 바라보는 시야를 기르는 것이 중요하다고 하셨다.
학습 키워드
Dependency DirectionLoose Coupling특강 목표
- 계층형 구조와 계층별 역할과 책임을 이해합니다.
- 단순한 패키지 분리가 아닌, 의존성 방향과 느슨한 결합의 중요성을 이해합니다.
의존성 방향을 한 방향으로 흐르게 한다.
상위 계층의 변경은 하위 계층에 영향을 주지 않는다.
역할 분리를 통해 수정이 발생하더라도 다른 계층에 영향이 가지 않아 변경이 용이해진다.
기본 구조
- controller
- service
- repository
- entity
- dto
도메인 기준 구조
- order
- controller
- service
- repository
- entity
- dto
- product
- controller
- service
- ...
각 계층이 자기 책임에만 집중하고,
다른 계층의 변경으로부터 보호받는 구조를 만드는 것.
예를 들어,
이러한 모든 변경이 비즈니스 로직에 영향을 주지 않는 상태가
이상적인 계층 분리가 달성된 구조이다.
정리하면,
Layered Architecture는 역할을 나누고 의존성 방향을 정리한다는 점에서 분명한 장점이 있다.
하지만 실제 개발을 하다 보면 Service 계층이 점점 Repository나 외부 기술에 의존하게 되는 경우가 많다.
예를 들어,
이러한 문제를 보완하기 위해 등장한 개념이 Hexagonal Architecture (Ports and Adapters) 이다.
Hexagonal Architecture는
비즈니스 로직을 애플리케이션의 중심에 두고,
외부 기술(DB, API, 메시지 브로커 등)을 바깥으로 밀어낸다.
핵심 개념은 다음과 같다.
즉, 기술이 도메인을 감싸는 구조가 아니라
도메인을 보호하는 구조를 만든다.
이 구조를 통해
와 같은 기술적 변화가 발생하더라도
비즈니스 로직에는 영향을 주지 않는 구조를 지향한다.
다만, 모든 프로젝트에 Hexagonal Architecture가 필요한 것은 아니다.
구조가 복잡해질 수 있기 때문에, 서비스의 규모와 확장 가능성을 고려한 뒤 적용해야 한다.
운영 비용, 팀원의 학습 곡선, 아키텍처를 적용하는 이유를 충분히 고민해보고 사용해야 한다.
이 서비스가 어디까지 확장될 가능성이 있는지,
서비스의 특징은 무엇인지 분석한 뒤에,
문제가 발생하는지 아키텍처의 필요성과 효과를 충분히 생각해야 한다.
아키텍처의 본질적인 목적은 변경에 유연한 시스템을 구성하는 것이다.
이러한 문제를 해결하기 위해 아키텍처를 도입하게 된다.
기술 외적인 측면도 고려해야 한다.
예를 들어, 투자 유치, 팀 확장, 서비스 확장 가능성 등도 함께 고민해야 한다.
서비스 디스커버리는 마이크로 서비스 아키텍처에서 서비스 위치(주소, 포트 등)를 동적으로 관리하고 제공하는 역할을 한다.
- 각 서비스는 시작 시 서비스 디스커버리 서버(예: Eureka, Consul)에 자신의 위치를 등록(register)
- 다른 서비스가 통신할 때, 직접 URL을 하드코딩하지 않고 서비스 디스커버리 클라이언트를 통해 서비스의 위치를 조회(discovery)
- 서비스 주소가 바뀌어도 클라이언트는 항상 최신 위치를 알 수 있어 유연성 및 확장성이 높아진다.
강의에서 그림을 보니 네트워크에서의 DNS의 역할과 비슷하다고 생각되었다.
Eureka는 넷플릭스가 개발한 서비스 디스커버리 서버로,
모든 서비스 인스턴스의 위치를 저장하는 중앙 저장소 역할을 하며,
서비스 인스턴스의 상태를 주기적으로 확인하여 가용성을 보장한다.
Eureka 서버는 서비스 레지스트리를 구성하는 중앙 서버이다.
#application.yml
server:
port: 8761
eureka:
client:
register-with-eureka: false # 다른 Eureka 서버에 이 서버를 등록하지 않음
fetch-registry: false # 다른 Eureka 서버의 레지스트리를 가져오지 않음
server:
enable-self-preservation: false # 자기 보호 모드 비활성화

package com.spring_cloud.eureka.server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableEurekaServer // 이 애플리케이션이 유레카 서버로 동작하도록 설정
@SpringBootApplication
public class ServerApplication {
public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
}
}
spring.application.name=server
server.port=19090
# 유레카 서버에 자신을 등록할지 여부를 설정합니다.
# true로 설정하면 유레카 클라이언트가 유레카 서버에 자신을 등록합니다.
# 유레카 서버에서는 일반적으로 false로 설정하여, 서버가 자기 자신을 등록하지 않도록 합니다.
eureka.client.register-with-eureka=false
# 유레카 서버로부터 레지스트리를 가져올지 여부를 설정합니다.
# true로 설정하면 유레카 클라이언트가 유레카 서버로부터 다른 서비스 인스턴스 목록을 가져옵니다.
# 유레카 서버에서는 일반적으로 false로 설정하여, 레지스트리를 가져오지 않도록 합니다.
eureka.client.fetch-registry=false
# 유레카 서버 인스턴스의 호스트 이름을 설정합니다.
# 유레카 서버가 자신의 호스트 이름을 다른 서비스에 알릴 때 사용합니다.
eureka.instance.hostname=localhost
# 유레카 클라이언트가 유레카 서버와 통신하기 위해 사용할 기본 서비스 URL을 설정합니다.
# 클라이언트 애플리케이션이 유레카 서버에 연결하고 등록하거나 레지스트리를 가져올 때 사용할 URL을 지정합니다.
eureka.client.service-url.defaultZone=http://localhost:19090/eureka/
# application.yml
spring:
application:
name: my-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/ # Eureka 서버 URL
register-with-eureka: true # Eureka 서버에 등록
fetch-registry: true # Eureka 서버로부터 레지스트리 정보 가져오기
instance:
hostname: localhost # 클라이언트 호스트 이름
prefer-ip-address: true # IP 주소 사용 선호
lease-renewal-interval-in-seconds: 30 # 리스 갱신 간격
lease-expiration-duration-in-seconds: 90 # 리스 만료 기간
| first | second |
|---|---|
![]() | ![]() |
spring.application.name=first
server.port=19091
# 유레카 클라이언트가 유레카 서버와 통신하기 위해 사용할 기본 서비스 URL을 설정합니다.
# 유레카 서버의 포트와 호스트 이름을 정확히 지정해야 합니다.
eureka.client.service-url.defaultZone=http://localhost:19090/eureka/
spring.application.name=second
server.port=19092
# 유레카 클라이언트가 유레카 서버와 통신하기 위해 사용할 기본 서비스 URL을 설정합니다.
# 유레카 서버의 포트와 호스트 이름을 정확히 지정해야 합니다.
eureka.client.service-url.defaultZone=http://localhost:19090/eureka/
spring-cloud-starter-netflix-eureka-client의존성을 사용하고, 애플리케이션 이름만 설정파일에 있으면 Eureka 서버에 자신의 위치를 등록한다.
클라이언트 애플리케이션은 Eureka 서버에서 필요한 서비스의 위치를 조회한다.

http://localhost:19090/ 으로 접속하면 두개의 인스턴스가 있는것을 확인할 수 있다.

Eureka 서버가 주기적으로 서비스 인스턴스의 상태를 확인하여 가용성을 유지한다.
기본 헬스 체크 엔드포인트로는 /actuator/health를 사용한다.
서비스 장애 시 Eureka 서버는 해당 인스턴스를 레지스트리에서 제거하여 다른 서비스의 접근을 차단한다. (로직은 따로 구성해야한다.)
로드 밸런싱은 네트워크 트래픽을 여러 서버로 분산시켜 서버의 부하를 줄이고, 시스템의 성능과 가용성을 높이는 기술이다.
클라이언트가 직접 여러 서버 중 하나를 선택하여 요청을 보내는 방식이다.
클라이언트는 서버의 목록을 가지고 있으며, 이를 바탕으로 로드 밸런싱을 수행한다.
로드밸런스를 사용하려면 REST 템플릿에 로드밸런스 어노테이션을 추가해야 한다.
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
@Bean
@LoadBalanced // 로드 밸런싱 추가
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
서비스의 URL에서 호스트 이름(애플리케이션 네임)으로 실제 인스턴스를 식별한다.
@RestController
public class MyRestTemplateController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/get-data-rest")
public String getDataWithRestTemplate() {
String serviceUrl = "http://my-service/api/data";
return restTemplate.getForObject(serviceUrl, String.class);
}
}
Feign : “~인 척하다, 가장하다, 꾸미다”
Spring Cloud에서 제공하는 HTTP 클라이언트로, 선언적으로 RESTful 웹 서비스를 호출할 수 있다.
Eureka와 같은 서비스 디스커버리와 연동하여 동적으로 서비스 인스턴스를 조회하고 로드 밸런싱을 수행할 수 있다.
넷플릭스가 개발한 클라이언트 사이드 로드 밸런서로, Eureka와 같은 서비스 디스커버리로부터 서비스 인스턴스 리스트를 제공받아 로드 밸런싱에 사용한다.
다양한 로드 밸런싱 알고리즘을 지원하며, 요청 실패 시 다른 인스턴스로 자동 전환하는 Failover를 지원한다.
# eureka 설정
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
...
# Ribbon 사용 설정
my-service:
ribbon:
eureka:
enabled: true
@FeignClient(name = "my-service") 어노테이션은 Eureka에 등록된 서비스 이름을 참조my-service라는 이름으로 등록된 서비스 인스턴스 목록을 조회FeignClient를 사용하기 위해서 dependency를 추가해준다.
dependencies {
...
implementation 'org-springframework-cloud: spring-cloud-starter-openfeign'
}
애플리케이션에서 @EnableFameClient 선언이 필요하다.
@SpringBootApplication
@EnableFeignClients // FeignClient 사용
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
인터페이스에 @FeignClient name속성으로 애플리케이션 이름을 지정해 유레카 서버에서 해당 인스턴스를 찾도록 한다.
Spring은 @FeignClient 어노테이션을 보고 런타임 시점에 구현체 인스턴스를 만들어 준다.
기존 Layered Architecture의 ServiceInterface - ServiceImpl 관계가
FeignClient인터페이스 - instance 관계로 바뀐것이라고 이해했다.
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam;
@FeignClient (name = "my-service") // 호출하려는 대상 호스트
public interface MyServiceClient {
@GetMapping ("/endpoint") //엔드포인트 정의
String getResponse (@RequestParam (name = "param") String param);
}
}
Controller에서는 인터페이스를 다음과 같이 사용한다.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MyController {
@Autowired
private MyServiceClient myServiceClient;
@GetMapping("/call-service")
public String callService(@RequestParam String param) {
return myServiceClient.getResponse(param);
}
}
각 서버에 순차적으로 요청을 분배하는 방식으로, 간단하고 공평하게 트래픽을 분산한다.
가중치 기반 로드 밸런싱: 각 서버에 가중치를 부여하고, 가중치에 비례하여 요청을 분배하는 방식이다. 서버의 성능이나 네트워크 상태에 따라 가중치를 조절한다.
현재 연결된 클라이언트 수가 가장 적은 서버로 요청을 보내는 방식이다.
서버의 응답 시간을 기준으로 가장 빠른 서버로 요청을 보내는 방식이다.

| Product | Order |
|---|---|
![]() | ![]() |
| 실행 포트: 19092,19093,19094 (VM옵션 -Dserver.port=N 으로 설정) JVM System Property가 application.yml보다 우선순위 | 19091 |
Main메서드가 있는 Application 파일에 @EnableFeignClients 어노테이션을 추가해준다.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ProductController {
@Value("${server.port}") // 애플리케이션이 실행 중인 포트를 주입받습니다.
private String serverPort;
@GetMapping("/product/{id}")
public String getProduct(@PathVariable String id) {
return "Product " + id + " info!!!!! From port : " + serverPort ;
}
}
spring:
application:
name: product-service
server:
port: 19092
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka/
@SpringBootApplication
@EnableFeignClients
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
@Service
@RequiredArgsConstructor
public class OrderService {
private final ProductClient productClient;
public String getProductInfo(String productId) {
return productClient.getProduct(productId);
}
public String getOrder(String orderId) {
if(orderId.equals("1") ){
String productId = "2";
String productInfo = getProductInfo(productId);
return "Your order is " + orderId + " and " + productInfo;
}
return "Not exist order...";
}
}
@FeignClient(name = "product-service")
public interface ProductClient {
@GetMapping("/product/{id}")
String getProduct(@PathVariable("id") String id);
}
spring:
application:
name: order-service # 잘못된 들여쓰기!
server:
port: 19091
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka/

(회색빛은 InteliJ 오류인듯)




Order를 표시하기 위한 Product 정보를
Order 애플리케이션이 Product에게 짬때렸다.
Spring Cloud에서 Feign + Eureka를 사용하면 내부적으로 클라이언트 사이드 로드밸런서가 자동으로 붙고 기본 알고리즘은 Round Robin이다.
새로고침을 해보면 Product 서버가 사이좋게 한 번씩 번갈아 응답하는 것을 알 수 있다.

Eureka 서버에서 불러오는 Application 이름은 프로퍼티 파일에서 설정한 이름이었다. 이름 정보가 나타나지 않자 UNKNOWN이름으로 유레카 서버에 등록됐다.
spring:
application:
name: order-service # 잘못된 들여쓰기!
server:
port: 19091
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka/

실행구성을 추가 하던 중 JVM 실행 변수 입력 칸을 착각해서 프로파일 부분에 작성했다가 IDE가 멈췄는데😭

강제 종료 후 다음 번에 같은 포트번호로 애플리케이션을 구동할 때 포트 점유 문제가 생긴다. MSA 환경 프로젝트를 할 때 자주 사용할 것 같아 해결방법을 적어둔다.
kill -9 @(lsof -t i:포트번호)
포트번호 범위로 강제 종료
kill -9 $(lsof -t -i:19090-19100)