728x90

Java에서 멀티스레드 환경을 개발하다 보면, 스레드마다 독립적인 데이터를 저장하고 관리해야 할 때가 있습니다.

이럴 때 유용하게 활용할 수 있는 클래스가 바로 ThreadLocal입니다.

 

이번 글에서는 ThreadLocal의 개념, 사용 방법, 그리고 실제 사례를 중심으로 살펴보겠습니다.

ThreadLocal이란?

ThreadLocal은 각 스레드마다 별도의 변수를 저장할 수 있도록 지원하는 Java 클래스입니다.

 

동일한 ThreadLocal 인스턴스를 사용하더라도, 각 스레드가 자신만의 독립적인 값을 가지며, 다른 스레드와 공유되지 않습니다.

즉, 하나의 변수를 스레드 간에 안전하게 관리할 수 있는 메커니즘을 제공합니다.

ThreadLocal의 특징

  1. 각 스레드마다 독립적인 데이터 저장 공간을 제공합니다.
  2. 스레드 간 데이터 공유를 방지하며, 멀티스레드 환경에서 데이터 충돌을 줄입니다.
  3. 특정 스레드에 한정된 데이터를 저장할 때 유용합니다.

ThreadLocal의 사용 방법

ThreadLocal은 주로 다음과 같은 메소드를 사용하여 데이터를 저장하고 접근합니다.

set()

현재 스레드의 ThreadLocal 변수에 값을 저장합니다.

ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("Thread A's Data");

get()

현재 스레드의 ThreadLocal 변수에 저장된 값을 가져옵니다.

String data = threadLocal.get();
System.out.println(data);

remove()

현재 스레드의 ThreadLocal 변수에 저장된 값을 삭제합니다. 이는 메모리 누수를 방지하기 위해 권장되는 작업입니다.

threadLocal.remove();

initialValue()

ThreadLocal 변수를 초기화할 때 사용됩니다. 기본적으로 null을 반환하지만, 필요하면 오버라이딩하여 초기값을 설정할 수 있습니다.

ThreadLocal<Integer> threadLocal = new ThreadLocal<>() {
    @Override
    protected Integer initialValue() {
        return 0;
    }
};
System.out.println(threadLocal.get());

사용 시 주의사항

ThreadLocal은 사용 후 반드시 remove() 메소드를 호출하여 데이터를 정리해야 합니다.

이를 소홀히 하면, 스레드가 종료된 후에도 ThreadLocal 객체에 데이터가 남아 메모리 누수가 발생할 수 있습니다.

 

또한, 너무 많은 데이터를 ThreadLocal에 저장할 경우 OOM을 야기할 수 있으니 적절하게 사용하는 것을 권장합니다.

 

ThreadLocal은 부모 Thread가 자식 Thread로 데이터를 전달하지 않습니다.

따라서 데이터를 상속하기 위해서는 ThreadLocal를 상속한 InheritableThreadLocal를 사용하시면 됩니다.


사용해 보기

ThreadLocal 정의

@Getter
@Setter
public class CustomContext {
    private static final ThreadLocal<CustomContext> CONTEXT = ThreadLocal.withInitial(CustomContext::new);

    private Object data;

    public static CustomContext getContext() {
        return CONTEXT.get();
    }

    public static void clearContext() {
        CONTEXT.remove();
    }
}

JWT Token 유저 ID ThreadLocal에 저장 및 사용 후 정리

@Slf4j
@Component
@RequiredArgsConstructor
public class AuthenticationInterceptor implements HandlerInterceptor {

    private final JwtTokenProvider jwtTokenProvider;
    private final CustomContext customContext = CustomContext.getContext();;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
        if(!(handler instanceof HandlerMethod))
            return true;

        String token = request.getHeader("authorization");
        if(Strings.isEmpty(token)) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "JWT token is missing");
            return false;
        }

        token = token.substring(7);
        if(!jwtTokenProvider.validateToken(token)) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "JWT Token is invalid");
            return false;
        }
        String userId = jwtTokenProvider.getUserId(token);
        customContext.setData(userId);

        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        CustomContext.clearContext();
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

ThreadLocal 데이터 사용

@Getter
@Setter
public class CreateQuoteRequest {
    transient private String userId;
    private long amount;
    private CurrencyCode targetCurrency;

    public CreateQuoteRequest() {
        CustomContext context = CustomContext.getContext();
        Assert.notNull(context);
        this.userId = (String) context.getData();
    }
}

후기

기존 ID를 넘겨줄 때 Token에서 뽑아 HttpServletRequest에 담거나 하여 사용을 하였지만,

이와 같이 ThreadLocal을 사용하게 된다면 본인 Thread 내에서 데이터를 쉽게 뽑아 사용할 수 있다는 점이 매력적으로 다가온 기술입니다.

 

물론 메모리에 올려두고 사용한다는 점에서 비효율적일 수 있지만, 코드를 생각했을 때 좀 더 보기 편하고 관리하기 편하다는 점이 부정할 수 없다 생각하는데요.

 

추후 좀 더 관리하기 좋은 방법을 찾게 된다면 포스팅하도록 하겠습니다. 

728x90

'Java' 카테고리의 다른 글

오차 없는 실수 연산 어떻게 해야 할까?  (0) 2025.01.10
[Java] Static  (3) 2024.12.26
[ Basic ] I/O Stream이란?  (0) 2024.12.23
[JPA] 비관적 락 낙관적 락  (0) 2024.06.29
JPA는 어떤 기술일까?  (0) 2024.04.12
728x90

최근 아키텍처 공부를 하면서 흔히 접할 수 있는 Clean ArchitectureHexagonal Architecture를 만나게 되었습니다.

 

여러 블로그들과 글들을 읽으면서 Clean Architecture와 Hexagonal Architecture의 다른 점이 무엇인지 점점 모호해지는 것을 느꼈습니다.

 

왜냐면 두 아키텍처에서는 Entity를 중심에 두고 그다음 UseCase를 통해 로직을 정의 한 다음 그다음 DIP를 적용하여 의존성을 내부 Core로 의존관계를 형성하면서, 외부의 변경 ( API변경, DB변경 등 )으로부터 내부 구현체들은 안전하다는 장점을 띄고 있기 때문입니다.

 

그렇다면 두 아키텍처는 어떤 점에서 차이를 보이고 있는지 알아보도록 하겠습니다.


Clean Architecture - Robert C. Martin

클린 아키텍처를 한 번이라도 보았다면 많이 익숙한 그림일 것입니다.

해당 그림은 의존관계를 내부로 의존하도록 하는 것을 가시화하여 보여주고 있습니다.

 

그림과 같은 의존관계를 형성한다면 파란색 원 즉 UI, Web, DB와 같은 외부 시스템 변경이 있다 하더라도 Application내부에는 영향이 없는 관계를 형성할 수 있습니다.

 

즉, 우리가 보호하려 하는 도메인의 정책 및 비즈니스 규칙들이 보호된다는 강점을 가질 수 있습니다.

 

클린 아키텍처에서는 다음과 같은 특징을 가집니다.

  • 프레임워크에 독립적이다.
  • 비즈니스 로직들 즉 내부 로직들은 외부 통신 없이 테스트 가능하다.
  • 내부 영향 없이 쉽게 외부 시스템을 변경할 수 있다.
  • 데이터베이스 종류와 관계없이 변경할 수 있다.
  • 외부 시스템에서는 Application 내부의 일에 대해 알 수 없다.

이러한 특징들은 외부와 분리된 흔히 모듈화 된 Application를 만들 수 있다는 점이 있습니다.

 

그럼 클린 아키텍처에서는 어떤 점을 중요하게 보아야 할까? 바로 종속성 규칙을 가장 중요하게 보면 될 거 같습니다.

종속성 규칙이 유효하다면 위 그림에 존재하는 원의 개수가 달라진다 하더라도 클린 아키텍처를 따르고 있다고 볼 수 있습니다.

 

다만, 종속성 규칙을 위배한다면 클린 아키텍처를 온전히 적용하였다고 보기는 어려울 것 같습니다.


Hexagonal Architecture - Alistair Cockburn

해당 아키텍처는 2005년에 처음 소개하면서 "Ports and Adapters"아키텍처라고 불렸습니다.

처음 소개 된 이름을 보면 감이 오시겠지만 핵사고 날 아키텍처에서 가장 중요한 개념은 Ports와 Adapters입니다.

 

Ports는 시스템이 외부와 통신하는 인터페이스를 정의하며,

AdaptersPorts와 외부 시스템 간의 실제 구현을 담당합니다.

 

즉, 이 또한 DIP를 활용하여 Application에서 외부에 의존하지 않고, Ports라는 Interface를 제공함으로써 Application이 하나의 모듈화 되는 아키텍처입니다.

 

그렇다면 왜 이 아키텍처 이름은 "Hexagonal Architecture"라 불리게 되었는지 의문이 들게 됩니다.

왜 "Hexagonal"인가?

  1. 다양한 외부 인터페이스 표현
    육각형은 여러 방향에서 포트를 연결할 수 있는 모양을 상징합니다. 이는 애플리케이션이 다양한 외부 시스템과 상호작용할 수 있다는 점을 강조합니다.
  2. 대칭성
    육각형의 대칭성은 애플리케이션이 외부 시스템과의 관계에서 특정 방향에 종속되지 않고, 내부 도메인 로직이 외부 의존성에서 독립적이어야 한다는 철학을 나타냅니다.
  3. 시각적 단순화
    Cockburn은 육각형이 포트와 어댑터를 배치하는 데 직관적인 도형이라고 보았습니다. 이 도형은 외부 시스템과 내부 도메인 간의 관계를 명확히 표현할 수 있습니다.

위와 같은 이유로 "Hexagonal Architecture"로 불리고 있습니다.

 

그럼 위 그림을 보면 Adapters두 육각형을 연결하는 역할 즉, 실질적인 구현체라는 것은 알겠지만,

Ports는 어디에 있는 걸까요?

 

그림을 자세히 보면 내부 육각형의 면이 굵은 것을 확인할 수 있습니다.

바로 이 육각형의 면이 외부 시스템과 연결을 위해 제공되는 Interface 즉, Ports입니다.

 

Hexagonal Architecture는 추후 Clean Architecture와 Onion architecture의 철학에 영향을 미치게 됩니다.


Clean Architecture와 Hexagonal Architecture의 차이

두 아키텍처는 기본적으로 같은 목표를 가진 설계 철학을 가지고 있어 혼동하기 쉽습니다.

 

그게 그거 아닌가!라고 생각 할 수 있지만, 딱 잡고 말하자면

Clean Architecture는 애플리케이션을 여러 계층으로 나누고, 의존성 방향을 바깥에서 안쪽으로 향하는 아키텍처이고,

Hexagonal ArchitecturePortsAdapters를 통해 외부 시스템과 내부 시스템을 분리하여 의존 방향이 Application 내부로 향하는 아키텍처입니다.

 

 즉, 두 아키텍처는 유지보수성과 확장성을 높이고, Core를 외부 시스템으로부터 분리하는데 초점을 맞춘다는 공통점이 존재합니다.

 

그럼 어떤 아키텍처를 사용하는 것이 좋을까요?

 

만약 애플리케이션의 종속성 규칙을 엄격하게 지키고 싶다면 Clean Architecture를,

외부 시스템과 내부 시스템의 엄격한 분리를 원한다면 Hexagonal Architecture를,

만약 둘 다 엄격하게 가져가고 싶다면 섞어 사용하면 될 것 같습니다.

 

결국 우리가 이야기하는 아키텍처는 하나의 시스템을 설계하는 방법일 뿐 상황에 맞춰 최선의 방법으로 설계하는 것이 가장 중요하다 생각합니다.

 

물론, 다른 개발자들을 설득하는 것은 본인의 몫이라는 것은 비밀입니다. 


Reference

Clean Architecture

Hexagonal Architecture

 

728x90
728x90

컴퓨터 프로그래밍에서 실수 연산은 흔히 오차 문제를 동반합니다.

 

특히 금융, 과학 계산, 그리고 데이터 분석 등 정확한 수치가 중요한 분야에서는 이 문제가 더욱 부각되는 문제인데요.

이번 글에서는 Double의 한계, Float를 사용한 실수 연산의 특징, 그리고 BigDecimal을 활용한 오차 없는 실수 연산 방법을 살펴보겠습니다.


1. Double의 문제: 부동소수점 연산의 한계

Double은 대부분의 프로그래밍 언어에서 기본 실수형으로 사용됩니다.

 

이는 64비트 IEEE 754 표준을 따르며, 빠른 연산 속도와 넓은 범위의 숫자를 처리할 수 있는 장점이 있습니다.

하지만 정확도 면에서는 한계가 있습니다.

public class DoubleExample {
    public static void main(String[] args) {
        double a = 0.1;
        double b = 0.2;
        double c = a + b;

        System.out.println("0.1 + 0.2 = " + c);
    }
}

출력 결과.

0.1 + 0.2 = 0.30000000000000004

왜 이런 일이 발생할까요?

  • Double은 2진수 기반의 부동소수점 표현 방식을 사용합니다.
  • 10진수 0.1과 0.2를 정확히 표현할 수 없기 때문에, 이진수로 변환될 때 근삿값으로 저장됩니다.
  • 이로 인해 연산 결과에도 미세한 오차가 발생합니다.

2. Float 사용: 더 작은 범위, 더 큰 오차

Float는 Double보다 작은 32비트 부동소수점 자료형입니다.

메모리 사용량이 적고 연산 속도가 빠르지만, 표현할 수 있는 유효 숫자 범위가 좁습니다.

public class FloatExample {
    public static void main(String[] args) {
        float a = 0.1f;
        float b = 0.2f;
        float c = a + b;

        System.out.println("0.1f + 0.2f = " + c);
    }
}

출력 결과.

0.1f + 0.2f = 0.3

겉보기에는 정확해 보이지만, 실제로는 내부에서 근삿값을 저장하고 있어 더 작은 숫자 범위에서 더 큰 오차가 발생할 가능성이 높습니다.

Float의 주요 특징

  • 적합한 경우: 높은 정밀도가 요구되지 않는 경우 (예: 게임 개발, 그래픽 연산).
  • 부적합한 경우: 금융 계산이나 통계 연산 등 오차가 치명적인 경우.

3. BigDecimal 사용: 실수 연산의 정밀도 보장

BigDecimal은 Java에서 제공하는 클래스 중 하나로, 정확한 실수 연산을 보장하기 위해 사용됩니다.

이 클래스는 실수를 10진법 기반으로 처리하므로, 부동소수점의 근삿값 문제를 해결할 수 있습니다.

import java.math.BigDecimal;

public class BigDecimalExample {
    public static void main(String[] args) {
        BigDecimal a = new BigDecimal("0.1");
        BigDecimal b = new BigDecimal("0.2");
        BigDecimal c = a.add(b);

        System.out.println("0.1 + 0.2 = " + c);
    }
}

출력 결과.

0.1 + 0.2 = 0.3

중요 사항

  • new BigDecimal(double)로 생성할 경우 부동 소수점의 오차가 그대로 발생할 위험성이 존재합니다.
  • "scale()"메서드로 소수 자릿수를 설정하여 계산 결과를 정리할 수 있습니다.

그럼 뭘 사용해야 할까?

실수 연산은 문제 되는 상황을 인지하고 있지 않다면 쉽게 간과할 수 있는 중요한 이슈입니다.

 

특히 환율, 이율 등과 같이 금전적인 부분이 연결되어 있다면 이는 서비스의 신뢰도를 낮추게 되는 매우 크리티컬 한 문제가 될 수 있습니다.

 

그렇기 때문에 위에 주어진 Double, Float, BigDecimal들을 잘 활용하여 적재적소에 사용해야 하는 것은 중요한 기본이라 생각합니다.

728x90

'Java' 카테고리의 다른 글

[ThreadLocal] Thread영역에 데이터를 저장하고 싶어요  (1) 2025.01.27
[Java] Static  (3) 2024.12.26
[ Basic ] I/O Stream이란?  (0) 2024.12.23
[JPA] 비관적 락 낙관적 락  (0) 2024.06.29
JPA는 어떤 기술일까?  (0) 2024.04.12
728x90

여러분, 혹시 친구와 나눈 대화를 기억하지 못해서 당황했던 적 있으신가요?

 

"어? 네가 그 얘기 했었어? 난 기억이 안 나는데..."

 

이런 상황이 발생하면 뭔가 민망하지만, 상대는 이런 대화를 Stateless한 태도로 받아들일 수 있을지도 모릅니다.

 

오늘은 이런 "기억하지 않는 대화"와 비슷한 Stateless, 그리고 그와 연관된 Connectionless에 대해 이야기해볼까 합니다.


Stateless란 무엇인가요?

Stateless는 말 그대로 "상태를 기억하지 않는 것"입니다.
쉽게 말해, 여러분이 음식점에서 주문할 때를 떠올려봅시다.

  1. 여러분: "김치찌개 주세요!"
  2. 직원: "네"
  3. 직원: "누구 김치찌개 시켰나요?"
  4. 여러분: "저요!"

직원은 여러분의 상태를 기억하지 않습니다.
다시 말해, 여러분이 누구인지, 이전에 뭘 주문했는지에 대한 정보는 그때그때 잊혀지는 거죠.
HTTP도 딱 이와 같습니다.

HTTP는 요청과 응답 사이에 상태를 저장하지 않습니다.
즉, Stateless한 프로토콜입니다.


Connectionless란 무엇인가요?

Connectionless는 연결을 유지하지 않는다는 의미입니다.
마치 음식점에서 음식을 주문하고 나면, 직원이 테이블 옆에 서서 대기하지 않는 것과 비슷하죠.

  1. 여러분: "김치찌개 주세요!"
  2. 직원: "네" (메모한 뒤 떠남)

직원이 계속 옆에 서 있으면 더 빨리 요구를 들어줄 수도 있겠지만,
그렇게 하면 다른 손님들에게 서비스를 제공하지 못하게 되는 문제가 생깁니다.

HTTP의 Connectionless도 마찬가지입니다.

클라이언트(브라우저)와 서버는 요청을 보낸 뒤 연결을 끊고, 다음 요청 때 다시 연결을 시작합니다.


HTTP는 왜 Stateless를 채택했을까요?

"왜 기억을 안 하지?"라고 궁금할 수 있습니다.
그 이유는 간단합니다: 확장성과 효율성 때문이죠.

  • 확장성:
    상태를 기억하지 않으면 서버는 요청이 올 때마다 독립적으로 처리할 수 있습니다.
    많은 사용자가 동시에 접속해도, 서로의 상태를 신경 쓰지 않아도 되니 시스템이 더 단순해지고 확장성이 높아집니다.
  • 효율성:
    상태를 저장하려면 많은 메모리와 자원이 필요합니다.
    기억해야 할 정보가 많아질수록 서버는 더 느려지겠죠?
    하지만 Stateless구조라면 이런 부담을 줄일 수 있습니다.

여기서드는 의문!

 

"확장성과 효율성을 위해 Stateless하고, Connectionless하면 요청 할 때마다 연결을 끊고 다시 맺을텐데,

성능의 문제가 생길 수 있지 않을까요?"

 

이를 해결하기 위해 "Keep-Alive"가 등장하게 됩니다.


HTTP의 Keep-Alive: "계속 얘기하자!"

음식점 상황을 다시 생각해 봅시다.

  • 기본 HTTP 방식:
    직원이 주문을 받을 때마다 떠났다가 다시 돌아옵니다.
  • HTTP Keep-Alive 방식:
    "저 김치찌개 말고 밥도 추가요!"
    직원이 "네, 계속 말씀하세요!"라고 하며 테이블을 계속 오갑니다.

Keep-Alive는 여러 요청을 처리할 때 연결을 유지한 채 대화를 이어가도록 해줍니다.
그 덕분에 매번 연결을 새로 맺을 필요가 없어 성능이 개선됩니다.


TCP의 Keep-Alive와 HTTP의 Keep-Alive의 차이

HTTP에서의 Keep-Alive와 TCP의 Keep-Alive는 어떤 차이가 있는지 알아보겠습니다.

 

이제 조금 더 깊이 들어가서, TCP Keep-AliveHTTP Keep-Alive의 차이를 알아봅시다.

  1. TCP Keep-Alive:
    TCP 레벨에서 네트워크 연결이 끊어졌는지 확인하는 작은 패킷을 주기적으로 보내는 기능입니다.
    즉, "연결이 살아 있나?"를 체크합니다.
  2. HTTP Keep-Alive:
    HTTP 요청과 응답을 처리할 때 연결을 유지하는 기능입니다.
    "새로운 연결을 맺지 말고 기존 연결을 재사용하자"는 뜻입니다.

비유하자면,

  • TCP Keep-Alive는 친구가 계속 살아있는지 확인하는 안부 전화이고,
  • HTTP Keep-Alive는 한 번에 많은 대화를 효율적으로 끝내는 방법입니다.

 

이러한 기능들은 우리가 사용하는 HTTP프로토콜에서 간단하게 확인해 볼 수 있는데요.

 

HTTP 1.1버전 기준으로 Connection속성keep-alive가 Default로 설정되어있고,

이에 대해 keep-Alive는 60초Timeout을 가지게 됩니다.


Reference

https://developer.mozilla.org/en-US/docs/Web/HTTP/Overview

https://en.wikipedia.org/wiki/HTTP_persistent_connection

https://blog.naver.com/whdgml1996/222153047879

 

728x90
728x90

우리가 웹브라우저 주소창에 도메인 주소(ex. www.naver.com)를 입력하고 나면, 

특정 사이트로 페이지를 이동하는 것을 확인할 수 있습니다.

 

하지만 개발을 하다 보면 도메인 주소 즉 사이트의 이름으로는 사이트가 인터넷이라는 바다의 어디에 있는지 알 수 있는 방법이 없습니다.

 

그렇다면 우리는 어떻게 해당 사이트를 찾아갈 수 있는 것일까요??

 

오늘은 사이트 이름을 가지고 해당 사이트의 주소를 찾아가는 이야기를 다뤄보려 합니다.


Domain과 IP의 개념

우선 우리가 왜 주소를 찾아야 하는지 알기 위해서는 Domain IP가 무엇을 의미하는지 아는 것부터 시작할 수 있습니다.

 

흔히 우리는 친구 집을 찾아간다 할 때 뭐라고 부르나요?

##네 집이라고 부르지 않나요??

 

이때 고민해봐야 하는 점은 왜 우리는 친구의 집서울특별시...라고 자세하게 말하지 않고 ##네 집이라고 부르는 것일까요?

 

여러 이유가 있겠지만 이미 어디인지 알기 때문에 간단하게 기억할 수 있는 이름으로 기억하는 것이 가장 크지 않을까요?

 

이처럼 우리는 이미 특정 위치를 말할 때는 부르기 쉬운 것으로 기억한다는 것을 알 수 있습니다.

 

그럼 다시 주제로 돌아와 IP는 어떤 것일까요?

IP는 인터넷에 특정 위치에 있는 사이트의 주소를 나타내게 됩니다.

 

그렇다면 Domain는 어떤 것일까요?

Domain는 사이트의 주소를 부르기 쉬운 명칭이라 볼 수 있습니다.

 

이로써 가벼운 개념은 정리가 된 것 같네요.

이 글에서 IP는 친구집 주소, Domain은 친구집 호칭으로 봐주시면 좋을 것 같습니다.


IP를 찾기 위한 모험

그렇다면 만약 전학생의 집에 놀러 가고 싶어 주소가 필요하다면 어떻게 해야 할까요?

 

우리는 이미 전학생네 집이라는 Domain을 가지고 있습니다.

만약 학교라면 선생님께 여쭤보고, 선생님이 모른다면 전산시스템에 들어갈 것이고, 이도 모른다면 못 찾아갈 것입니다.

또한, 만약 주소가 잘못되었다면 우리는 결국 찾아가지 못할 것입니다.

 

이러한 시스템은 인터넷에서도 동일한데요.

우리가 사용하는 컴퓨터에 hosts라는 파일은 우리가 Domain을 방문하고 싶을 때 해당 IP정보를 가지고 있는 파일입니다.

하지만, 여기에 존재하지 않을 때 우리 컴퓨터는 누구에게 물어볼 수 있을 까요?

1. Caching Resolver

캐싱 리졸버는 사용자의 로컬 네트워크에 있는 서버이며, 우리가 사이트에 한번 들어가게 되면 해당 IP와 Domain은 Caching Resolver에 저장되어 다음번 방문 때 훨씬 빠르게 도와주는 서버입니다.

 

즉 우리가 빠르게 접근할 수 있는 긴급연락망이 되겠네요!

2. Local DNS

해당 서버는 기지국 DNS라고도 부르며, 우리가 흔히 사용하는 통신사(SKT, KT, LG 등...)를 의미합니다.

 

로컬 PC에서 사이트 IP를 찾지 못한 경우 Local DNSIP를 요청하게 되며, 존재하지 않을 경우 다음 단계로 넘어가게 됩니다.

 

해당 서버는 교무실이라고 볼 수 있겠네요!

3.  Root DNS

이런!! 교무실에 방문했더니 다른 학년이라 학생 정보가 없는 걸까요?!

 

그럼 우린 더 많은 정보를 접할 수 있게 교육청에 찾아가도록 하죠!

Root DNS는  ICANN이 관리하는 DNS시스템의 최상위 계층에 존재하는 서버입니다.

 

Root DNS에서 우리가 원하는 전학생네 집 주소가 있는지 확인합니다.

이때! 있으면 정말 다행이지만, 없다면 동사무소에 찾아가 보기로 합니다.

4. TLD( Top-Level-Domain ) DNS

여기서 동사무소"www.naver.com"에 접속한다 하면,

맨 뒤에 작성된 ". com"을 소유한 주소들을 관리하는 서버입니다. 

 

즉, ". com"를 소유한 주소들을 가진 서버에서 확인한다는 의미입니다.

 

TLD DNS에서 DomainIP를 찾아보고 없다면 하위 Second Level Domain DNS로 넘겨 최종적으로 www.naver.com  Domain DNS Authoritative DNS Server에 접근하여 IP를 얻어낼 수 있습니다.

 

그럼 우리는 모든 상황에서 전학생네 집을 찾았을까요??

아쉽게도 하나의 가정이 더 남아있네요...

 

만약, 동사무소도 주소를 모른다면 어떻게 할까요?

 

물론 이미 해당 레벨에서 한 번이라도 사람이 방문한 적 있다면 주소를 얻을 수 있겠지만,

만약 없다면 친구는 무인도에 살고 있을 확률이 높으므로 다음날 학교에서 물어보도록 합시다.


여행의 끝

우리는 위의 전학생 주소 찾기를 통해 어떻게 주소를 찾아야 할지를 비교적 간단하게 알아보았습니다.

 

추가적인 내용으로는 국가 코드, 일반 코드 등 상세하게 나눠지는 부분은 당연히 존재합니다.

하지만, 이 글의 목적은 어떤 흐름으로 Domain으로 IP를 가져오는지에 대한 니즈를 해결하기 위한 글이므로 다음 고급 편에서 더욱 세세하게 다뤄보도록 하겠습니다.

 

이렇게 IP를 가져왔다면 IP를 해석하며 서버를 찾아가는 과정도 존재하겠죠?

해당 내용은 "너에게 닿기를"이란 주제로 찾아뵙겠습니다.


Reference

https://www.cloudflare.com/ko-kr/learning/dns/what-is-dns/

https://aws.amazon.com/ko/route53/what-is-dns/

DNS 트래픽 쿼리 관련 논문: https://arxiv.org/pdf/2308.07966

 

https://namu.wiki/w/%EB%84%88%EC%9D%98%20%EC%9D%B4%EB%A6%84%EC%9D%80.

 

너의 이름은.

아직 만난 적 없는 너를, 찾고 있어. まだ会ったことのない君を、探している。 캐치프레이즈 2016년 8월 26일

namu.wiki

 

728x90
728x90

1. Static이란??

Static 키워드를 선언하게 되면 사용할 때만 메모리에 할당하고, 사용 안 할 때는 제거되는 일반 선언들과 달리 프로그램이 종료될 때 까지 메모리에 할당되어 있는 것을 의미합니다.

 

그렇기 때문에 우리가 Static으로 선언한 것들은 따로 생성하지 않고 가져다 사용할 수 있는 것입니다.

 

그렇다면 우리가 흔히 사용하는 Static 변수와 Static 메소드들은 어떤 작동원리를 가지고, 어떤 차이점이 존재하는지 알아보도록 하겠습니다.


2. Static 변수

Static 변수는 클래스 수준에서 선언되며, 인스턴스와 관계없이 모든 객체가 동일한 메모리 공간을 공유합니다. Static 변수는 클래스가 메모리에 로드될 때 한 번만 초기화되며, 프로그램이 종료될 때까지 메모리에 유지됩니다.

Static 변수의 특징

1. 공유 메모리
Static 변수는 클래스 로더가 클래스를 로드할 때 메모리에 할당되며, 해당 클래스의 모든 인스턴스가 이 변수를 공유합니다. 따라서 인스턴스마다 별도의 값을 가지지 않고, 모든 인스턴스가 동일한 값을 참조합니다.

 

2. 클래스 이름으로 접근 가능
Static 변수는 객체를 생성하지 않고, 클래스 이름을 통해 직접 접근할 수 있습니다.

 

3. 메모리 효율성
여러 객체에서 공통으로 사용하는 데이터를 Static 변수로 선언하면, 각 객체가 별도의 메모리를 할당받지 않으므로 메모리 효율성을 높일 수 있습니다.

 

4. 초기화 시점
Static 변수는 프로그램 실행 시점에 클래스가 메모리에 로드될 때 초기화됩니다. 한 번만 초기화되며, 이후 변경된 값은 프로그램 종료 시점까지 유지됩니다.


Static 변수 사용 사례

1. 공유 데이터 관리
객체생성과 관계없이 데이터를 공유하고 싶다면 Static변수를 사용하여 관리할 수 있습니다.

 

2. 상수 선언

변하지 않는 상수 값을 정의할 때 Static 변수를 final 키워드와 함께 사용하여 메모리 낭비를 줄일 수 있습니다.

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    static final int MAXIMUM_CAPACITY = 1 << 30;
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
}


3. 유틸리티 클래스에서 공용 변수 사용
특정 설정 값이나 공용 데이터를 유틸리티 클래스에 Static 변수로 선언해 다른 클래스에서 쉽게 접근하도록 합니다.

public final class StandardCharsets {
    private StandardCharsets() {
        throw new AssertionError("No java.nio.charset.StandardCharsets instances for you!");
    }
    public static final Charset US_ASCII = sun.nio.cs.US_ASCII.INSTANCE;
    public static final Charset ISO_8859_1 = sun.nio.cs.ISO_8859_1.INSTANCE;
    public static final Charset UTF_8 = sun.nio.cs.UTF_8.INSTANCE;
    public static final Charset UTF_16BE = new sun.nio.cs.UTF_16BE();
    public static final Charset UTF_16LE = new sun.nio.cs.UTF_16LE();
    public static final Charset UTF_16 = new sun.nio.cs.UTF_16();
}

3. Static 메소드

Static 메소드는 객체를 생성하지 않고도 호출할 수 있는 클래스 수준의 메소드입니다. Static 변수와는 다르게 메소드는 로직을 정의하며, Static 키워드로 선언된 메소드는 클래스의 모든 객체가 동일한 동작을 공유합니다.

Static 메소드의 특징

1. 객체 없이 호출 가능
Static 메소드는 클래스 이름으로 호출할 수 있습니다.

 

2. Static 변수와 연동 가능
Static 메소드는 같은 클래스에 선언된 Static 변수에 직접 접근할 수 있습니다.

 

3. 인스턴스 멤버 사용 불가
Static 메소드 내에서는 클래스의 인스턴스 변수나 인스턴스 메소드를 직접 사용할 수 없습니다. 이는 Static 메소드가 클래스 수준에서 동작하기 때문입니다. 인스턴스 멤버를 사용하려면 해당 메소드에서 객체를 생성하거나 참조를 전달받아야 합니다.

 

4. 상속과 재정의 제한
Static 메소드는 상속받은 클래스에서 오버라이드(재정의)할 수 없습니다. 하지만 동일한 이름으로 정의해 숨길 수는 있습니다.


Static 메소드 사용 사례

1. 유틸리티 메소드 제공
자주 사용되는 공용 로직들을 Static 메소드로 정의해 간편하게 호출할 수 있습니다.

 

2. 팩토리 메소드 구현
특정 객체를 생성하는 로직을 Static 메소드로 제공할 수 있습니다.

 

3. 프로그램의 진입점
Java 프로그램은 main 메소드를 Static으로 선언하여, 객체를 생성하지 않고 프로그램의 실행을 시작합니다.

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

 

728x90
728x90

I/O 스트림이란?

I/O 스트림은 Java에서 데이터를 입력(Input)하거나 출력(Output)할 때 사용하는 추상화된 모델입니다.

  • InputStream: 데이터를 읽어오는 데 사용.
  • OutputStream: 데이터를 외부로 쓰는 데 사용.

특징

  • 데이터의 흐름을 Stream으로 간주.
  • Byte 단위 또는 Character 단위로 처리.
  • 데이터 소스: 파일, 네트워크 소켓, 메모리 등.

💡 "Java의 I/O는 Stream 기반이다. 데이터를 한 번에 처리하지 않고, 스트림으로 데이터를 흘려보내면서 효율적으로 작업한다."


InputStream과 OutputStream의 기본 구조

InputStream의 주요 메서드

 
int read() throws IOException  
int read(byte[] b, int off, int len) throws IOException  
void close() throws IOException

OutputStream의 주요 메서드

void write(int b) throws IOException  
void write(byte[] b, int off, int len) throws IOException  
void close() throws IOException
 

주요 I/O 클래스와 사용 사례

파일 읽기: FileInputStream

 
import java.io.FileInputStream;
import java.io.IOException;

public class FileInputExample {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("example.txt")) {
            int data;
            while ((data = fis.read()) != -1) {
                System.out.print((char) data); // 데이터를 문자로 변환해 출력
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

파일 쓰기: FileOutputStream

import java.io.FileOutputStream;
import java.io.IOException;

public class FileOutputExample {
    public static void main(String[] args) {
        try (FileOutputStream fos = new FileOutputStream("example.txt")) {
            String data = "Java I/O Stream 예제입니다.";
            fos.write(data.getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

🔑 TIP: try-with-resources를 사용하면 스트림을 자동으로 닫아 메모리 누수를 방지할 수 있습니다.


Buffered 스트림과 성능 향상

BufferedInputStream 사용 예

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;

public class BufferedInputExample {
    public static void main(String[] args) {
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("example.txt"))) {
            int data;
            while ((data = bis.read()) != -1) {
                System.out.print((char) data);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

왜 Buffered 스트림을 사용할까?

  • 기본 스트림은 데이터를 1바이트씩 처리.
  • Buffered 스트림은 버퍼를 사용해 데이터 블록을 한꺼번에 처리 -> 성능 향상.
728x90
728x90

레드-블랙 트리란?

레드-블랙 트리란 BTS의 일종으로, BTS의 최악의 시간복잡도 O(n)을 방지하기 위해 균형을 유지하게 하는 균형 이진 탐색트리 중 하나입니다.

FunctionSearchInsertDelete

Function Amortized Worst Case
Select O(log⁡n) O(log⁡n)
Insert O(log⁡n) O(log⁡n)
Delete O(log⁡n) O(log⁡n)

 

레드-블랙 트리의 속성

레드-블랙 트리의 노드들은 red 또는 black색을 가지며, 아래와 같은 5가지 특징을 준수하여 균형을 이룹니다.

  1. 모든 노드는 red 혹은 black이다.
  2. 루트 노드는 black이다.
  3. 모든 리프 노드( NIL )는 black이다.
  4. red 노드의 자식 노드는 항상 black이다.
  5. 모든 노드에서 리프 노드들 까지 가는 경로에 존재하는 black노드의 수는 같다. ( 검색 시작 노드 제외 )

데이터 삽입

우선 레드-블랙 트리에서 삽입하는 노드의 색은 항상 red입니다.

 

데이터 삽입 과정 중 레드 - 블랙 트리의 속성에 위배하는 경우 Rebalancing과정을 거치게 됩니다.

Rebalancing은 색변경(Recoloring)회전(Rotation) 과정으로 이루어져 있으며, 케이스에 따라 두 가지 방법을 혼합하여 적용하면 레드 - 블랙 트리의 속성을 위반하지 않게 만들 수 있습니다.

 

아래에 삽입 시 발생할 수 있는 케이스에 대해 알아보고 또한 Rebalancing과정이 어떻게 이루어질 수 있는지 알아보도록 하겠습니다.

Case1. Root 삽입

맨 처음 Root에 데이터를 삽입하는 Case입니다.

 

데이터 삽입 시 들어오는 노드의 색은 항상 red로 고정되어 있기 때문에 삽입된 노드의 색상은 red입니다.

이때, 레드-블랙 트리 2번째 속성 'root는 black이다'를 위반하기 때문에 Recoloring과정을 통해 black으로 변경하여 줍니다.


Case2. 삽입한 노드의 부모 노드가 black인 경우

데이터 삽입 시 삽입되는 노드의 색상은 red이고, 위반하는 규칙은 존재하지 않습니다.


Case3. 부모 노드가 red이고, 한쪽으로 쏠린 경우

1을 삽입하게 되면 위 사진의 첫번째와 같이 red하위에 red가 존재하는 양상의 띄게 되며 그와 동시에 4번 속성을 위반하게 됩니다.

 

그럴경우 부모노드 색을 black으로 조부모 색을 red로 변경해줍니다.

그 다음 왼쪽으로 쏠림현상이 존재하므로 5를 기준으로 오른쪽으로 회전하여 2가 root에 위치하도록 합니다.

 

수정 후 모습을 보면 4번 속성을 위반하지 않고, 2번 속성도 위반하지 않는 것을 확인 할 수 있었습니다.


Case4. 부모노드가 red이고, 한쪽으로 몰려있으며, 오른쪽으로 꺾인 경우

3을 삽입하게 되면 오른쪽으로 꺾이게 됩니다.

이때 삽입한 노드를 기준으로 좌로 회전합니다.

 

이후 root와 삽입한 노드의 색을 바꿔주고, root를 기준으로 오른쪽으로 회전합니다.


Case5. 부모노드가 red이고, 삼촌노드도 red인 경우

6을 삽입하였습니다.

우선 4번 속성을 위반하기 때문에 부모와 삼촌 노드의 색을 black으로, 조상 노드의 색을 red로 변경합니다.

 

이후 2번 속성을 위반하였기 때문에 루트 노드의 색을 black으로 변경해줍니다.

 

이때 black 하위 black인 경우가 생기게 되는데, 이는 속성을 위반하는 사항이 아니므로 괜찮습니다.


이후 많은 다양한 상황들이 나올 수 있지만, 위 케이스들을 기반으로 비교하면 레드-블랙 트리의 속성을 지키며, 작업을 이어나갈 수 있게 됩니다.


레드-블랙 트리  VS AVL 트리

레드 블랙 트리를 왜 사용하는지, 그리고 AVL 트리와의 차이는 어떤 점들이 존재하는지 알아보도록 하겠습니다.

  레드-블랙 트리 AVL 트리
삽입/삭제 속도 더 빠름 (재조정이 덜 복잡함) 느림 (더 엄격한 균형 유지)
검색 속도 약간 느림 ( 완벽한 균형 x )  더 빠름 (더 균형 잡힘)
구현 난이도 상대적으로 단순 더 빠름(더 균형 잡힘)
적합한 용도 삽입/삭제가 잦은 경우 일기 성능이 중요한 경우
사용처 TreeMap, TreeSet,... Index, Cache,...

Reference

https://en.wikipedia.org/wiki/Red%E2%80%93black_tree

https://www.youtube.com/watch?v=2MdsebfJOyM

 

 

 

728x90

+ Recent posts