본문 바로가기
Everyday Study

2024.09.03(화) { 원자성, 바인딩, 모듈, perthis, 자바 에이전트 }

by xogns93 2024. 9. 3.

원자성(Atomicity)

 

Java에서 원자성(Atomicity)은 멀티스레딩 환경에서 여러 스레드가 동시에 공유 자원에 접근할 때 데이터 일관성을 보장하는 중요한 개념입니다. 원자성 있는 연산은 분할될 수 없는 작업으로, 다른 스레드가 그 연산이 완료되기 전까지 중간 상태를 볼 수 없도록 보장합니다.

 

Atomic 클래스

 

  • Java에서는 java.util.concurrent.atomic 패키지에서 원자성 있는 연산을 지원하는 다양한 클래스를 제공합니다. 이 클래스들은 내부적으로 CAS(Compare-And-Swap) 같은 저수준 동기화 기법을 사용하여, 안전한 멀티스레딩을 지원합니다.
  • 주요 Atomic 클래스:
    • AtomicInteger: 원자적 int 연산을 제공합니다.
    • AtomicLong: 원자적 long 연산을 제공합니다.
    • AtomicBoolean: 원자적 boolean 연산을 제공합니다.
    • AtomicReference: 원자적 참조 연산을 제공합니다.
import java.util.concurrent.atomic.AtomicInteger;

public class Counter {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet();  // 원자적 증가 연산
    }

    public int getCount() {
        return count.get();  // 현재 값을 원자적으로 가져옴
    }

    public static void main(String[] args) {
        Counter counter = new Counter();

        // 여러 스레드가 동시에 increment 메서드를 호출
        for (int i = 0; i < 1000; i++) {
            new Thread(counter::increment).start();
        }

        // 잠시 대기하여 모든 스레드가 완료되도록 함
        try { Thread.sleep(100); } catch (InterruptedException e) {}

        // 최종 count 값을 출력
        System.out.println("Final count is: " + counter.getCount());
    }
}

 

설명

  • AtomicInteger: 이 클래스는 내부적으로 원자성을 보장하는 int 타입의 변수를 관리합니다.
  • incrementAndGet(): 이 메서드는 count 값을 원자적으로 증가시키며, 이 연산은 중간 상태를 노출하지 않습니다.
  • 스레드 안전성: 여러 스레드가 동시에 increment() 메서드를 호출해도 AtomicInteger 덕분에 count 값이 정확하게 증가합니다.

 


Java에서의 바인딩

  • 정적 바인딩(컴파일 타임 바인딩):
    • 메서드 오버로딩
    • 변수 참조
    • 클래스 메서드 및 변수
  • 동적 바인딩(런타임 바인딩):
    • 메서드 오버라이딩
    • 인터페이스와 추상 클래스의 구현체 호출

바인딩의 중요성

  • 성능: 정적 바인딩은 컴파일 타임에 결정되므로, 동적 바인딩보다 빠릅니다. 동적 바인딩은 런타임에 메서드 호출을 결정해야 하기 때문에 오버헤드가 발생할 수 있습니다.
  • 유연성: 동적 바인딩은 다형성을 가능하게 하여 프로그램의 유연성을 높입니다. 객체의 실제 타입에 따라 적절한 메서드가 호출되므로, 코드의 재사용성과 확장성이 증가합니다.

 


모듈

 

모듈(Module)은 자바 9에서 도입된 개념으로, 프로그램을 더 잘 조직화하고 관리하기 위한 새로운 단위입니다. 자바의 모듈은 여러 개의 패키지와 클래스들을 그룹화하여 하나의 독립적인 단위로 묶어주는 것입니다. 모듈을 사용하면 애플리케이션의 규모가 커지더라도, 코드를 더 쉽게 관리하고, 필요하지 않은 부분은 숨길 수 있습니다.

자바 모듈의 주요 개념

  1. 모듈은 패키지의 묶음:
    • 자바 모듈은 여러 패키지를 하나의 논리적인 단위로 묶어줍니다. 예를 들어, 애플리케이션의 사용자 인터페이스, 데이터베이스 처리, 비즈니스 로직을 각각의 모듈로 나눌 수 있습니다.
  2. 캡슐화 강화:
    • 모듈은 내부에서 사용하는 패키지와 클래스를 외부에 공개할지 여부를 결정할 수 있습니다. 공개하지 않으면, 그 모듈 안에서만 사용할 수 있게 되어, 다른 모듈에서는 접근할 수 없습니다.
    • 이로 인해 코드의 캡슐화가 강화되며, 모듈 간의 의존성을 명확하게 정의할 수 있습니다.
  3. 모듈 선언 파일(module-info.java):
    • 각 모듈에는 module-info.java라는 특별한 파일이 있어, 그 모듈이 어떤 패키지를 외부에 공개할지, 다른 모듈에 의존하는지 등을 정의합니다.

델리게이트(Delegation)

 

델리게이트(Delegation)와 관련된 개념은 객체 지향 프로그래밍에서 많이 사용되는 패턴으로, 책임을 다른 객체로 위임하는 것을 의미합니다. 이 개념은 특히 AOP(Aspect-Oriented Programming)와 프록시 패턴에서 자주 등장합니다. 각 요소에 대해 설명하겠습니다.

 

1. 위임받는 애 (타겟 메서드, Target Method)

  • 설명: 위임받는 애, 즉 타겟 메서드실제로 작업을 수행하는 메서드입니다. 이 메서드는 원래의 로직을 포함하고 있으며, 위임(Delegation)을 통해 프록시 객체나 다른 객체가 이 메서드를 호출하게 됩니다.
  • 예시: 비즈니스 로직을 처리하는 performTask()라는 메서드가 있을 때, 이 메서드가 타겟 메서드입니다.
  • 역할: 타겟 메서드는 최종적으로 작업을 수행하는 책임을 가집니다.

2. 위임하는 애 (프록시, Proxy)

  • 설명: 프록시(Proxy)는 실제 작업을 수행하는 타겟 메서드에 대한 접근을 제어하거나, 추가적인 로직을 적용하는 객체입니다. 프록시는 클라이언트와 타겟 메서드 사이에 위치하여 호출을 가로채거나, 추가적인 동작을 수행한 후 타겟 메서드를 호출합니다.
  • 예시: performTask() 메서드 호출 전에 로깅을 하거나, 트랜잭션을 시작하는 프록시 객체가 있을 수 있습니다.
  • 역할: 프록시는 클라이언트와 타겟 메서드 사이에서 중재 역할을 하며, 부가적인 기능(예: 로깅, 보안 검사)을 제공할 수 있습니다.

3. 인트로덕션을 구현하는 애 (Introduction Implementor)

  • 설명: 인트로덕션(Introduction)은 AOP에서 사용되는 개념으로, 기존 클래스에 새로운 메서드나 인터페이스를 동적으로 추가하는 것을 말합니다. 인트로덕션을 구현하는 애는 기존 클래스에 이러한 새로운 기능을 추가하는 역할을 합니다.
  • 예시: 예를 들어, Auditable이라는 인터페이스를 구현하지 않은 클래스에 이 인터페이스를 동적으로 추가하여, setAuditInfo()와 같은 메서드를 추가할 수 있습니다.
  • 역할: 인트로덕션을 통해 기존 객체에 새로운 행동을 동적으로 부여하여, 객체의 인터페이스를 확장하거나 새로운 기능을 제공할 수 있습니다.

 

예시:

interface Task {
    void performTask();
}

class RealTask implements Task {
    @Override
    public void performTask() {
        System.out.println("Performing the actual task.");
    }
}

class TaskProxy implements Task {
    private RealTask realTask;

    public TaskProxy(RealTask realTask) {
        this.realTask = realTask;
    }

    @Override
    public void performTask() {
        System.out.println("Before task execution (logging, security, etc.)");
        realTask.performTask();  // 타겟 메서드 호출
        System.out.println("After task execution (logging, etc.)");
    }
}

interface Auditable {
    void setAuditInfo(String info);
}

class TaskWithAudit implements Auditable {
    @Override
    public void setAuditInfo(String info) {
        System.out.println("Audit Info: " + info);
    }
}

설명:

  • 타겟 메서드 (performTask() in RealTask): 실제 작업을 수행하는 메서드입니다.
  • 프록시 (TaskProxy): TaskProxy는 performTask() 메서드 호출을 가로채고, 전후에 추가적인 로직을 수행한 후 타겟 메서드를 호출합니다.
  • 인트로덕션 (TaskWithAudit implementing Auditable): TaskWithAudit 클래스는 Auditable 인터페이스를 구현하여, 기존의 RealTask 클래스에 동적으로 새로운 기능을 추가할 수 있습니다.

요약

  • 위임받는 애 (타겟 메서드): 실제로 작업을 수행하는 메서드입니다.
  • 위임하는 애 (프록시): 타겟 메서드에 대한 호출을 가로채고, 추가적인 기능을 제공하는 객체입니다.
  • 인트로덕션을 구현하는 애: 기존 클래스에 새로운 기능이나 인터페이스를 동적으로 추가하여, 객체의 기능을 확장하는 역할을 합니다.

(exposeProxy = true)

 

exposeProxy(true)는 Spring 프레임워크에서 AOP(Aspect-Oriented Programming)를 사용할 때, 프록시(proxy) 객체를 코드 내부에서 접근할 수 있도록 노출(expose)하는 설정입니다. 이 설정은 특히 자기 자신(this) 호출과 관련된 문제를 해결하는 데 사용됩니다.

 

일반적으로 AOP 설정에서 적용합니다. 예를 들어, JavaConfig를 사용하여 AOP 설정을 할 때 다음과 같이 설정할 수 있습니다:

@Configuration
@EnableAspectJAutoProxy(exposeProxy = true)
public class AppConfig {
    // 기타 설정
}

위 설정에서 exposeProxy = true는 AOP 프록시를 노출시켜 AopContext.currentProxy()로 접근할 수 있도록 합니다.

 

 

import org.springframework.aop.framework.AopContext;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.stereotype.Component;

@Component
public class MyService {

    public void methodA() {
        System.out.println("Inside methodA");
        // 자기 자신 내에서 methodB 호출
        methodB();
    }

    public void methodB() {
        System.out.println("Inside methodB");
    }

    public void methodAWithProxy() {
        System.out.println("Inside methodAWithProxy");
        // 프록시를 사용하여 methodB 호출
        ((MyService) AopContext.currentProxy()).methodB();
    }
}

// AOP 설정 클래스에서 setExposeProxy(true)를 설정해야 합니다.
// 이 설정은 주로 XML 또는 JavaConfig에서 설정합니다.

설명:

  • methodA(): this.methodB()가 호출되면, 프록시를 거치지 않기 때문에 AOP 어드바이스가 적용되지 않습니다.
  • methodAWithProxy(): AopContext.currentProxy()를 사용하여 현재 프록시를 가져와, 프록시를 통해 methodB()를 호출합니다. 이 경우 어드바이스가 정상적으로 적용됩니다.

 


perthis

 

perthis는 AspectJ에서 사용되는 Aspect Instantiation Model 중 하나입니다. 이 모델은 Spring AOP에서 AspectJ의 특정 기능을 활용하여 Aspect(어드바이스가 포함된 클래스)의 인스턴스 생성 방식을 제어하는 방법입니다.

 

Aspect Instantiation Model : perthis

  • perthis 모델은 특정 조인 포인트에서 this 객체에 따라 새로운 어드바이스 인스턴스를 생성하는 방법입니다.
  • 즉, **"perthis"**를 사용하면, 타겟 메서드가 실행되는 객체(즉, this로 참조되는 객체)마다 별도의 어드바이스 인스턴스가 생성됩니다.

아래의 예시를 통해 perthis의 동작 방식을 이해할 수 있습니다:

@Aspect("perthis(execution(* com.example..*.*(..)))")
public class MyAspect {

    private int someState = 0;

    @Before("execution(* com.example..*.*(..))")
    public void beforeMethod() {
        someState++;
        System.out.println("Method called. Current state: " + someState);
    }
}

동작 방식

  • 위의 예시에서, perthis(execution(* com.example..*.*(..)))는 com.example 패키지 내의 메서드가 실행될 때, 해당 메서드를 호출하는 객체(this)마다 독립된 MyAspect 인스턴스를 생성합니다.
  • 예를 들어, A라는 객체와 B라는 객체가 있을 때, 둘 다 com.example 패키지 내의 동일한 메서드를 호출하더라도, A와 B는 각기 다른 MyAspect 인스턴스를 사용하게 됩니다.
  • 이렇게 되면 각 객체마다 고유한 someState 값을 유지할 수 있습니다. 즉, A가 메서드를 호출할 때마다 A의 someState가 증가하고, B가 메서드를 호출할 때마다 B의 someState가 증가합니다.

Spring AOP와의 관계

  • Spring AOP는 기본적으로 싱글톤 인스턴스 모델을 사용합니다. 즉, 어드바이스가 싱글톤으로 관리되며, 모든 조인 포인트에 대해 동일한 인스턴스가 사용됩니다.
  • 그러나 perthis와 같은 모델을 사용하면, AspectJ 스타일로 Spring AOP에서 각 객체마다 별도의 어드바이스 인스턴스를 가질 수 있게 됩니다.

 

perthis 쓰게되면 싱글톤이 아니기 때문에 거의 잘 쓰지않습니다. 대부분의 애플리케이션에서는 싱글톤 Aspect 인스턴스가 충분하며, 이는 개발, 유지보수, 성능 등 여러 측면에서 가장 효율적입니다.

 


자바에이전트 (Java Agent)  -  Spring InstrumentAspectJ

 

자바 에이전트(Java Agent) 개념

  • 자바 에이전트는 JVM(Java Virtual Machine)에 의해 관리되는 프로세스에 부착되어 동작을 수정할 수 있는 프로그램입니다.
  • 에이전트는 JVM이 시작될 때, 또는 애플리케이션 실행 중에 동적으로 로드되어 특정 클래스의 바이트코드를 조작할 수 있습니다.
  • 자바 에이전트는 java.lang.instrument.Instrumentation 인터페이스를 통해 제공되는 API를 사용하여 바이트코드 조작과 같은 기능을 수행합니다.
  • Spring Instrumentation:
    • 주로 스프링 AOP와 로드 타임 위빙을 지원하기 위해 사용됩니다.
    • spring-instrument.jar을 통해 JVM 시작 시 에이전트를 로드하여 바이트코드를 조작합니다.
    • 스프링 애플리케이션에서 AOP를 런타임에 적용할 때 유용합니다.
  • AspectJ:
    • 강력한 AOP 프레임워크로, 자바 코드에서 다양한 조인 포인트를 지원합니다.
    • AspectJ 에이전트는 JVM에 로드되어 위빙을 실행합니다.
    • 스프링과 함께 또는 독립적으로 사용될 수 있으며, 고급 AOP 기능을 제공합니다.

자바 에이전트는 이 두 프레임워크에서 핵심적인 역할을 하며, 동적 코드 변경과 런타임 AOP 적용을 가능하게 합니다. 각 프레임워크는 고유의 장점을 가지고 있으며, 특정 요구사항에 따라 선택적으로 사용될 수 있습니다.

 


비즈니스로직의 타겟이 되는애들이 - 도메인

 

예시

예를 들어, 전자상거래 애플리케이션에서는 다음과 같은 도메인과 비즈니스 로직이 있을 수 있습니다:

  • 도메인:
    • Customer: 고객을 나타내는 객체
    • Order: 주문을 나타내는 객체
    • Product: 제품을 나타내는 객체
  • 비즈니스 로직:
    • Order Processing: 고객이 주문을 생성하고, 결제하고, 제품을 배송받는 과정
    • Inventory Management: 제품 재고를 관리하고, 주문이 들어올 때 재고를 감소시키는 로직

여기서 Order Processing이라는 비즈니스 로직은 Order, Customer, Product와 같은 도메인 객체를 타겟으로 삼아 동작하게 됩니다.