반응형

1. 개요

 readObject 메서드의 역할을 이해하고, 방어적으로 작성해야하는 이유를 알아보자.

 

2. 직렬화, 역직렬화 예제

 일반적으로 Object를 직렬화할 땐 ObjectOutputStream을, 역직렬화 할땐 ObjectOutputStream 인스턴스 인스턴스를 사용하고, 각각 writeObject(), readObject() 메서드를 호출하여 실제 데이터를 처리한다. 간단한 예제를 통해 직렬화와 역직렬화의 흐름을 먼저 이해해보자.

1) Period 인스턴스에 대한 직렬화

 

 

public final class Period implements Serializable {

   private Date start;
   private Date end;

   public Period(Date start, Date end) {
       this.start = new Date(start.getTime()); // 방어적 복사
       this.end = new Date(end.getTime());
       if (this.start.compareTo(this.end) > 0) { // 유효성 검사
           throw new IllegalArgumentException(start + " after " + end);
       }
   }

   public Date start() {
       return new Date(start.getTime());
   }

   public Date end() {
       return new Date(end.getTime());
   }
}

 

SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
Date startDate = df.parse("2024-11-14");
Date endDate = df.parse("2024-11-15");
try(
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos)){

    oos.writeObject(new Period(startDate, endDate));

    byte[] serialized = baos.toByteArray();
    for(byte b : serialized){
        System.out.print(b +" ");
    }
}

 

결과화면은 다음과 같다.

 

직렬화되어 byte[] 타입으로 변환된 Period 인스턴스

 

 

2) 직렬화된 byte[] 데이터에 대한 역직렬화

    public static void main(String[] args) throws IOException, ParseException {

        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
        Date startDate = df.parse("2024-11-14");
        Date endDate = df.parse("2024-11-15");
        try(
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ObjectOutputStream oos = new ObjectOutputStream(baos)){

            oos.writeObject(new Period(startDate, endDate));  // 직렬화

            byte[] serialized = baos.toByteArray();

            Period period = deserializePeriod(serialized); // 역직렬화
            System.out.println(period.start());
            System.out.println(period.end());
        }
    }
    
    public static Period deserializePeriod(byte[] bytes) throws IOException {

        try(
                ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
                ObjectInputStream ois = new ObjectInputStream(bais)){

                Object object = ois.readObject();
                return (Period) object;
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

 

결과 화면은 다음과 같다.

역직렬화 후 메서드 출력 결과

 

 

3. readObject() 메서드의 문제 #1

readObject() 메서드가 수행하는 역할을 보면 byte[] 데이터를 기반으로 인스턴스를 생성하는 역할을 한다. 즉, 생성자의 역할을 하는 것이다. 일반적으로 생성자에는 validation을 통해 데이터의 불변식을 보장한다. 하지만 바이트 데이터가 조작되어 불변식이 깨진 Object에 대한 byte[] 데이터가 들어온다면 readObject() 메서드를 호출했을 때 별다른 validation 체크를 하지 않기때문에 불변식을 보장하지 못하게 된다.

public class BogusPeriod {
    
    // 정상적이지 않은 바이트스트림
    private static final byte[] serializedForm = {
        (byte)0xac, (byte)0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x06,
        0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x40, 0x7e, (byte)0xf8,
        0x2b, 0x4f, 0x46, (byte)0xc0, (byte)0xf4, 0x02, 0x00, 0x02,
        0x4c, 0x00, 0x03, 0x65, 0x6e, 0x64, 0x74, 0x00, 0x10, 0x4c,
        0x6a, 0x61, 0x76, 0x61, 0x2f, 0x75, 0x74, 0x69, 0x6c, 0x2f,
        0x44, 0x61, 0x74, 0x65, 0x3b, 0x4c, 0x00, 0x05, 0x73, 0x74,
        0x61, 0x72, 0x74, 0x71, 0x00, 0x7e, 0x00, 0x01, 0x78, 0x70,
        0x73, 0x72, 0x00, 0x0e, 0x6a, 0x61, 0x76, 0x61, 0x2e, 0x75,
        0x74, 0x69, 0x6c, 0x2e, 0x44, 0x61, 0x74, 0x65, 0x68, 0x6a,
        (byte)0x81, 0x01, 0x4b, 0x59, 0x74, 0x19, 0x03, 0x00, 0x00,
        0x78, 0x70, 0x77, 0x08, 0x00, 0x00, 0x00, 0x66, (byte)0xdf,
        0x6e, 0x1e, 0x00, 0x78, 0x73, 0x71, 0x00, 0x7e, 0x00, 0x03,
        0x77, 0x08, 0x00, 0x00, 0x00, (byte)0xd5, 0x17, 0x69, 0x22,
        0x00, 0x78
    };

    public static void main(String[] args) {
        Period p = (Period) deserialize(serializedForm);
        System.out.println(p);
    }
    
    static Object deserialize(byte[] sf) {
        try {
            return new ObjectInputStream(new ByteArrayInputStream(sf)).readObject();
        } catch (IOException | ClassNotFoundException e) {
            throw new IllegalArgumentException(e);
        }
    }
}

 

출력하면 다음과 같이 start보다 end가 더 이르다는 것을 확인할 수 있다. 생성자를 사용하지 않기 때문에 validation 체크가 이루어지지 않고, 최종적으로 생성된 Period 객체는 불변식이 깨져버렸다.

 


4. readObject() 메서드 커스텀

 

이를 해결하기 위해 readObject() 메서드를 생성해야한다. 역직렬화할 클래스 내에 private 접근 제어자로 readObject() 메서드가 정의되어 있을 경우 ObjectInputStream.readObject() 메서드 호출 시 새로 정의한 메서드를 호출하기 때문이다. (리플렉션 기반)

 단, 반드시 privcate 접근 제어자로 선언해야하고, 메서드 내에서 defaultWriteObject(),  defaultReadObject()와 같은 기본 직렬화, 역직렬화 메서드를 한번 호출해줘야한다.

 

public final class Period implements Serializable {
    private Date start;
    private Date end;

    public Period(Date start, Date end) {
        this.start = new Date(start.getTime()); // 방어적 복사
        this.end = new Date(end.getTime());
        if (this.start.compareTo(this.end) > 0) { // 유효성 검사
            throw new IllegalArgumentException(start + " after " + end);
        }
    }

    public Date start() {
        return new Date(start.getTime());
    }

    public Date end() {
        return new Date(end.getTime());
    }

    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException{
        s.defaultReadObject();
        if (this.start.compareTo(this.end) > 0) { // 유효성 검사
            throw new InvalidObjectException(start + " after " + end);
        }
    }
}

 

그 후, 불변식이 깨진 byte[] 데이터를 넣어 역직렬화를 해보면 유효하지 않는 오브젝트로 판단하여 역직렬화에 대한 예외를 발생시킨다.

역직렬화 실패

 


5. readObject() 메서드의 문제 #2

하나의 문제가 더 있다. 직렬화된 스트림에 객체 참조 관련 바이트코드를 추가하면, 가변 Period 인스턴스를 만들 수 있다. Period 인스턴스를 가변 인스턴스로 사용할 수 있게 하는 MutablePeriod 클래스를 만든다.

public class MutablePeriod {
   public final Period period;

   public final Date start;

   public final Date end;

   public MutablePeriod() {
       try {
           ByteArrayOutputStream bos = new ByteArrayOutputStream();
           ObjectOutputStream out = new ObjectOutputStream(bos);

           // 불변식을 유지하는 Period 를 직렬화.
           out.writeObject(new Period(new Date(), new Date()));

           /*
            * 악의적인 start, end 로의 참조를 추가.
            */
           byte[] ref = { 0x71, 0, 0x7e, 0, 5 }; // 악의적인 참조
           bos.write(ref); // 시작 필드
           ref[4] = 4; // 악의적인 참조
           bos.write(ref); // 종료 필드

           // 역직렬화 과정에서 Period 객체의 Date 참조를 훔친다.
           ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
           period = (Period) in.readObject();
           start = (Date) in.readObject();
           end = (Date) in.readObject();
       } catch (IOException | ClassNotFoundException e) {
           throw new AssertionError(e);
       }
   }

 

여기서 직렬화한 바이트 코드에 [0x71, 0, 0x7e, 0, 5], [0x71, 0, 0x7e, 0, 4] 코드를 추가한다. 이 값은 Java 직렬화 프로토콜에서 객체 참조를 나타내는 바이트 코드이다.

 

    public static void main(String[] args) {
        MutablePeriod mp = new MutablePeriod();
        Period mutablePeriod = mp.period; // 불변 객체로 생성한 Period
        Date pEnd = mp.end; // MutablePeriod 클래스의 end 필드
        
        pEnd.setYear(78); // MutablePeriod 의 end 를 바꿨는데 ?
        System.out.println(mutablePeriod.end()); // Period 의 값이 바뀐다.
        
        pEnd.setYear(69);
        System.out.println(mutablePeriod.end());
    }

 

결과적으로 이러한 방법을 사용하게 되면, 불변 객체로 생성되어야할 Period 인스턴스의 end 값이 수정됨을 알 수 있다.

이를 해결하기 위해서는 readObject() 메서드 재정의 시 방어적 복사 코드를 추가해야한다.

 

private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
   s.defaultReadObject();

   // 방어적 복사를 통해 인스턴스의 필드값 초기화
   start = new Date(start.getTime());
   end = new Date(end.getTime());

   // 유효성 검사
   if (start.compareTo(end) > 0)
       throw new InvalidObjectException(start +" after "+ end);
}

 

 

정리

 이런 문제는 누군가 고의적으로 바이트코드를 위조한 상황을 가정했을 때 발생하는 문제들이다. 직렬화한 코드를 아무나 접근할 수 있는 외부 서비스에 보냈을 때 불변식이 깨지고, 객체 참조까지 되는 것을 확인할 수 있다.

 앞선 챕터에서 Serializable은 신중하게 작성하라라는 내용이 있었다. 직렬화된 클래스가 외부로 나갔을 때 발생할 수 있는 여러 문제들이 있기 때문이다.

 이번 챕터 역시 이러한 문제를 겪지 않으려면 직렬화를 신중히 하고, 만약 직렬화가 필요하다면, 외부에서 접근할 수 없는 환경에 저장하고, 로드할 수 있도록 하는 것이 중요하다는 것을 다시 한번 느끼게 되었다.

반응형
반응형

1. 개요

직렬화(Serializable)의 개념을 이해하고, Serializable을 구현할 때 주의해야할 점을 알아보자.

 

2. 직렬화(Serializable)란?

 

인스턴스 >> Byte

 

인스턴스를 Byte로 변환하는 것을 말한다. 간혹 클래스를 Byte로 변환하는 것이라고도 하는데 목적 측면에서 보거나, 일반적으로 사용하는 방식을 보면 인스턴스를 직렬화하는게 더 맞는 표현이라고 생각한다.

 

 

3. 직렬화를 하는 이유

그렇다면 직렬화를 하는 이유는 뭘까? 도대체 뭘 위해서 직렬화를 하는걸까?

 

 

 

바로 인스턴스를 저장하거나, 전송하기 위함이다.

 

그럼 인스턴스를 저장하거나 전송하는 이유는 뭘까?

 

 

 

객체의 상태를 저장해야할 상황이 있기 때문이다. 아래 예시를 통해 알아보자.

 

4. 객체의 상태를 저장해야할 상황 

1) HTTP 세션 관리

 Spring 웹 어플리케이션에서는 HTTP 세션을 사용해 사용자 상태를 유지한다. 사용자의 ID나 이름, 국적 등과 같은 '상태값'을 세션에 저장해두고 필요할때마다 가져온다.

 만약 세션 클러스터링 환경이라면 어떨까? 세션을 다른 서버와 공유해야한다. 정확히 말하면 상태값을 갖고 있는 인스턴스를 다른 서버에 전달해야한다. 이를 위해 직렬화하고, Byte 데이터를 Stream 방식으로 전달하는 것이다.

 

2) 인메모리 저장소 활용(ex. Redis)

spring Session이나 캐싱기능을 위해 저장소로 Redis를 사용하고자 한다면, Redis에 저장할 인스턴스의 클래스에 Serializable을 구현했을 것이다. 상태 값을 가진 인스턴스를 Redis라는 외부 저장소에 전달해야하기 때문이다. 이때도 마찬가지로 외부로 전달하기 위해 직렬화를 한다.

 

3) 영속적인 저장 / 데이터 복원

 HTTP 세션, 클러스터링 환경, 인메모리 저장소는 서버가 재시작되면 저장된 데이터가 날아간다. 서버가 재시작되도 상태값을 유지하고 싶다면 상태 값을 필드로 구분하여 DB에 저장하는 방법도 있지만, 인스턴스 자체를 DB에 저장하는 방법도 있다. 디스크에 저장하는 방식이다. 이 경우 서버에 문제가 생기거나 재시작이 되어도 데이터를 쉽게 복원할 수 있다.

 

5. Serializable 주의사항

 

주의할 점이 많은 Serializable

1) 릴리스 한 뒤에는 수정이 어렵다.

클래스가 Serializable을 구현하면 직렬화된 바이트 스트림 인코딩도 하나의 공개 API가 된다. 누군가 역직렬화를 하면 클래스 구조와 상태값들을 확인할 수 있기 때문이다. 그래서 이 클래스가 널리 퍼지게 된다면, 그 직렬화 형태도 영원히 지원해야 한다. 만약 특정 필드를 제거하거나, 타입을 바꾸는 등의 수정을 한다면, 타 시스템의 역직렬화 과정에서 에러가 발생할 수 있기 때문이다.

직렬화 가능 클래스를 만들고자 한다면, 길게 보고 수정이 일어나지 않는 형태로 주의해서 설계해야한다.

 

2) 버그와 보안 구멍

객체는 생성자를 사용해 만드는게 기본이다. 역직렬화는 기본 방식을 우회하는 생성 기법이다. 마치 숨은 생성자와 같다. 생성자를 우회한다는건 객체 초기화에 필요한 모든 내부 코드를 무시한다는 측면에서 버그를 유발 가능성을 재기한다.

 또한 공격자가 객체 내부를 들여다 볼 수 있다면, 직렬화된 코드를 조작하여 서버로 전달할 수 있다. 해당 서버에서 조작된 메서드를 호출이라도 한다면 공격자가 의도한 작업을 서버 내에서 실행하게 될것이다.

따라서 공격자가 이러한 데이터를 볼 수 없는 환경, 즉 private한 환경에서 사용해야한다.

 

3) 신버전을 릴리스할 때 테스트가 늘어난다.

 직렬화 가능 클래스가 수정되면 신버전 인스턴스를 직렬화 한 후 구버전으로 역직렬화할 수 있는지, 그 반대도 가능한지 테스트 해야한다. 직렬화 가능 클래스의 수가 많을수록 테스트할 횟수도 증가한다.

 

4) 상속용으로 설계된 클래스나 인터페이스는 Serializable을 구현하거나 확장해선 안된다.

해당 클래스를 클래스를 상속하거나 구현하는 모든 클래스는 앞선 문제들에 위험을 안고가게 된다.

 

 

그런데... 직렬화 클래스를 수정하면 역직렬화가 무조건 실패하는게 아닐까? 필자의 경우도 그랬던 기억이 있었다. 

 

6. 직렬화 클래스를 수정하면 역직렬화가 무조건 실패하는거 아냐?

직렬화 클래스를 수정해도 구버전 데이터를 역직렬화할 수 있는 조건이 있다. 아래 두 조건을 모두 만족해야한다.

 

첫째, SerialVersionUID 가 일치할때

 클래스가 변경되었지만 serialVersionUID가 동일하다면 역직렬화가 가능하다. 클래스 구조가 약간 달라져도 serialVersionUID가 같다면 역직렬화를 시도할 수 있다.

 

둘째, 새로운 필드가 추가되거나 제거됐을 때

 - 직렬화된 클래스에 신규 필드를 추가할 경우, 역직렬화 시 해당 필드는 기본값 (null, 0, false) 등으로 설정된다.

 - 기존 필드가 삭제되어도 역직렬화가 가능하다. 역직렬화 시 해당 필드는 무시된다.

 

위 두 조건을 만족하면 직렬화 클래스가 수정되도 역직렬화가 가능하다.

단, 클래스의 인터페이스 혹은 상속 구조를 변경하거나, 필드 타입, 클래스 이름이 변경되면 역직렬화에 실패하게 된다.

 

serialVersionUID
클래스의 직렬 버전을 나타내는 ID 로 모든 직렬화된 클래스는 이 값을 부여받는다. 만약 필드에 명시한다면 그 값이 ID가 되겠지만, 명시하지 않는다면 클래스의 정보를 기반으로 해시함수를 적용해 UID가 자동으로 부여된다.

 

즉, 추후 조금의 수정 가능성을 염두해둔다면 SerialVersionUID의 값을 지정해주는 것이 좋다.

 

 

7. 정리

 Serializable은 구현하기는 아주 쉽지만, 주의해야할 사항이 많다. 때문에 클래스의 여러 버전이 상호작용할 일이 없고, 서버가 신뢰할 수 없는 데이터에 노출될 가능성이 없는 등, 보호된 환경에서만 쓰일 클래스가 아니라면 Serializable 구현은 아주 신중하게 이뤄져야한다. 상속할 수 있는 클래스라면 주의사항이 더욱 많아진다.

 

그럼 2만

퇴긘~

반응형
반응형

1. 개요

 우리가 설치하고, 쿼리를 작성하는 MySQL 서버는 크게 두 엔진으로 구성된다. 하나는 MySQL 엔진, 하나는 스토리지 엔진. 스토리지 엔진은 데이터를 저장하는 하드웨어와 관련된 엔진이고, MySQL 엔진은 그 외 알쏭달쏭한 모든것이라고 생각하면 된다. 

MySQL 엔진과 스토리지 엔진

 

뭔 개소린가 싶겠지만 일단은 이렇게 이해하고 시작해보도록 하자. 이게 정신건강에는 좋은것같다.


2. MySQL 전체 구조 핥아보기

 상당히 친해보이는 두 엔진이 MySQL 어디에 속하는지 알아보자. 아래는 Real MySQL8.0 에 기재된 MySQL 서버 구조이다.

 

MySQL 서버의 전체 구조

 

 우리의 몸에 여러 기관들이 있듯이 하나의 엔진에도 여러 요소들이 존재한다. 각 요소들을 살펴보기 전에 그림에 나온 MySQL 서버의 전체 구조를 이해해보자.

 


3. MySQL 서버 전체 구조

MySQL 서버 전체 구조는 크게 프로그래밍 API, MySQL 서버, 운영체제 하드웨어로 나뉜다.

1) 프로그래밍 API 

MySQL 서버로 들어오는 요청에 대한 API를 말한다. Java 진영의 백엔드 개발자 입장에서는 JPA, MyBatis 를 사용해 MySQL 서버로 연동하는 부분이라 생각하면 된다.

2) MySQL 서버

우리가 배워야할 MySQL 엔진과 스토리지 엔진으로 구성된 서버이다. 생소한 용어들이 대부분이나 InnoDB는 뭔가 익숙하다. 일단 넘어가자.

3) 운영체제 하드웨어

DB 데이터는 운영체제 내 디스크에 저장된다. 단순 DB 데이터 뿐 아니라 로그파일도 저장된다.


4. MySQL 엔진 요소

1) 커넥션 핸들러 

 MySQL을 설치하고 가장 먼저 MySQL 서버에 접속한다. 접속에 성공하면 MySQL 서버와 접속한 클라이언트 간 커넥션이 생성된다. 커넥션 핸들러는 이러한 커넥션을 생성하고 클라이언트의 쿼리 요청, 즉, 커넥션에 관한 처리들을 핸들링한다.

 여기서 중요한건 '쿼리 요청을 핸들링'하는 것이다. 커넥션 핸들러가 쿼리를 분석하여 '처리'하진 않는다. 쿼리 요청이 오면 쿼리를 처리하는 어떤 녀석에게 "select 쿼리 처리해주세요!, insert 쿼리 처리해주세요!" 처럼 요청할 뿐이다.

 

2) SQL 인터페이스

DML, DDL, 프로시져, 뷰, 커서, 트리거, 함수 등의 지원을 위한 인터페이스를 말한다.

 

3) 쿼리 파서 (SQL 파서)

 커넥션 핸들러로부터 들어온 요청을 처리하며, 요청받은 쿼리 문장을 토큰(MySQL이 인식할 수 있는 최소 단위의 어휘나 기호)으로 분리해 트리 형태의 구조로 만들어준다. 이러한 트리형태의 데이터를 '파서 트리'라고 하며, 이 과정에서 문법 오류를 체크한다.

 

4) SQL 옵티마이저

 파서 트리를 효율적으로 처리하기 위한 실행 계획을 결정한다. 여기서 결정된 실행 계획에 따라 쿼리 처리 속도나 비용이 달라지게 된다. MySQL 뿐만 아니라 모든 DBMS에서도 쿼리의 실행계획을 수립하는 옵티마이저를 갖고 있다. 실행 계획에 대해 잘 이해하여 쿼리의 불합리한 부분을 찾아낸다면 옵티마이저의 최적화에 도움을 줄 수 있을것이다.

 추후 실행계획에 대해서 공부한 후 포스팅하도록 하겠다.

5) 캐시

MySQL 에서는 InnoDB Buffer Pool, Table Open Cache, Thread Cache 캐시가 사용된다. 캐시에 대한 자세한 내용은 다음에 다루도록 하겠다.

 

* 제거된 쿼리 캐시
 MySQL 8.0 부터 쿼리 캐시 기능이 제거됐다. 쿼리 캐시는 빠른 응답을 필요로 하는 웹 기반의 응용 프로그램에서 매우 중요한 역할을 담당했었다. SQL의 실행 결과를 메모리에 캐시하고, 동일 SQL 쿼리가 실행되면 테이블을 읽지 않고 즉시 결과를 반환하도록 동작했다. 하지만 테이블의 데이터가 변경되면 캐시에 저장된 결과 중에서 변경된 테이블과 관련된 것들을 모두 삭제해야 했다. 이는 심각한 동시 처리 성능 저하, 버그를 유발해 결국 제거됐다.

6) 버퍼

Join, Order By, Group By, Select 등의 쿼리 실행 시 사용되는 임시 메모리 공간을 의미한다. 종류로 Sort Buffer, Join Buffer, Read Buffer 가 있다. 각각에 대해 알아보자.

 

- Sort Buffer

 ORDER BY나 GROUP BY를 사용할 때 사용되는 버퍼로, 정렬에 필요한 데이터를 메모리에 저장하고, 이 안에서 정렬을 수행한다. 만약 정렬할 데이터가 Sort Buffer에 담기지 않을 경우 일부 데이터를 디스크의 임시 파일에 저장하고, 메모리와 디스크를 함께 사용해 정렬한다.

 

* ORDER BY 시 Select 절에는 꼭 필요한 컬럼만 기재하자
Order By 사용 시 Select 절 "*" 를 넣거나 굳이 필요없는 컬럼 추가한다면 Sort Buffer 의 사이즈를 초과할 수 있다. 이 경우 디스크에 임시 파일 형태로 데이터들이 저장될것이고 많은 I/O 비용을 들이는 메모리&디스크 정렬이 수행되기 때문이다.

- Join Buffer

인덱스가 없는 조인 작업을 수행할 때 사용하는 버퍼이다. 두 테이블을 조인할 때 보다 작은 테이블의 데이터를 Join Buffer에 저장하고, 큰 테이블의 데이터를 반복해서 비교하는 방식이다. 만약 테이블의 데이터가 Join Buffer에 담기지 않을 경우 여러번에 나누어 조인을 수행한다. 이는 성능 저하를 초래한다.

 

* Join Buffer에 의존하기보단 인덱스 기반 조인을 사용하자
일반적으로 Join Buffer에 의존한 조인보다 인덱스 기반의 조인이 성능면에서 우수하다. 키나 인덱스로 설정되지 않는 컬럼에 대한조인 시, 속도가 느리다면 Join Buffer를 추가하는것보다 인덱스 설정을 고려하자. 

- Read Buffer

 테이블의 데이터를 순차적(Full Scan)으로 읽을 때 사용되는 버퍼이다. 규모가 큰 테이블일수록 버퍼의 효율이 높아진다.

* 규모가 클수록 효율이 높은 이유
 Full Scan이 발생하면 MySQL은 디스크에서 테이블 데이터를 한번에 읽어오는게 아닌 조금씩 읽어온다. 이 과정에서 I/O 작업이 발생하는데, 읽어야할 테이블의 데이터가 많을 수록 I/O 작업은 늘어나게 된다. Buffer를 사용하게 된다면 조금씩이 아닌 버퍼 사이즈만큼 읽어올 수 있으므로 버퍼의 효율이 높은것이다. read_buffer_size 옵션으로 조절 가능하며 기본 값은 128KB이다.

5. MySQL 쿼리 실행 구조

스토리지 엔진을 알아보기 전 MySQL 엔진에서 일어나는 쿼리 실행 프로세스에 대해 알아보자. 여기서 사용되는 개념은 앞선 MySQL 서버 전체 구조의 요소에 중복된 내용이 있을 수 있으니 복습한다 생각하고 참고하도록 하자.

 

MySQL 쿼리 실행 구조

1) 클라이언트 접속

 클라이언트가 MySQL 서버에 접속하기 위해 ID/PW를 입력한다. 입력하는 시점에  MySQL 네이티브 프로토콜을 통해 MySQL 서버와 TCP/IP 기반의 커넥션이 맺어지게 된다. 이때 커넥션이 생성된다는 뜻인데, 정확히는 '임시 커넥션'이 생성되며, ID/PW 인증이 성공할 경우 완전한 커넥션이 맺어지게 된다.

 

2) 쿼리 요청 처리

 클라이언트가 쿼리를 입력하면 커넥션 핸들러가 쿼리 요청을 처리하기 위해 쿼리를 쿼리 파서에게 전달한다.

 

3) 파서 트리 생성 및 문법 오류 체크

 쿼리 파서는 받은 쿼리를 토큰으로 분리해 파서 트리를 생성한다. 추가로 이 과정에서 문법 오류를 체크한다. 문법에 이상이 없으면 전처리기에게 파서 트리를 전달한다.

 

4) 개체 검사 및 권한 체크

 전처리기는 파서 트리를 분석해 쿼리 문장에 구조적인 문제점이 있는지 확인한다. 각 토큰을 테이블 명, 컬럼 명, 내장 함수 명 등과 같은 개체에 매핑하여 유효성을 검사하고, 접근 권한을 확인한다. 실제 존재하지 않거나, 권한 상 접근할 수 있는 개체의 토큰은 이 단계에서 체크한다. 그 후 옵티마이저에게 전달한다.

 

5) 실행 계획 수립

 옵티마이저는 쿼리를 분석해 실행 계획을 수립한다. 하지만 계획을 할당하고, 이행하는 주체는 따로 있다. 실행 엔진과 핸들러인데, 이들의 관계를 회사로 비유하면 아래와 같다.

 

 - 옵티마이저 : 회사의 경영진 (회사의 계획 수립)

 - 실행 엔진 : 중간 관리자 (계획을 받아 업무를 할당)

 - 핸들러 : 실무자 (업무를 이행)

 

 이 단계에서는 옵티마이저가 실행 엔진에게 실행 계획을 전달한다.

 

6) 핸들러에게 처리 요청

 실행 엔진은 (옵티마이저가 만든) 계획대로 각 핸들러에게 요청해서 받은 결과를 또 다른 핸들러의 요청의 입력으로 연결하는 역할을 수행한다.

 

7) 핸들러의 작업 처리

 핸들러는 MySQL 서버의 가장 밑단에서 MySQL 실행 엔진의 요청에 따라 데이터를 디스크로 저장하거나 읽어 오는 역할을 담당한다. 즉, 핸들러는 스토리지 엔진을 의미하며, MyISAM 테이블을 조작하는 경우에는 핸들러가 MyISAM 스토리지 엔진이 되고, InnoDB 테이블을 조작하는 경우에는 핸들러가 InnoDB 스토리지 엔진이 된다.

반응형
반응형

1. 개요

 하나의 자바 프로세스 안에서 여러 스레드이 동작한다. 이 스레드들은 스레드 스케줄러에 실행된다. 그렇다면 프로그램의 동작을 스레드 스케줄러에 기대지 말라는 뜻이 뭘까?

 

스레드 스케줄러
운영체제가 프로세스 내 스레드들 사이에서 CPU 시간을 어떻게 분배할지를 결정하는 주체이다. 자바 코드 레벨에서 스레드를 생성해도 마찬가지이다. 스레드 스케줄러의 정책에 따라 실행된다. 정책으로는 라운드 로빈 방식, 우선순위 기반 방식, 최소 남은 시간 우선 방식 등이 있다.

 


2. 운영체제마다 다른 스케줄링 정책

 스레드 스케줄러의 정책은 운영체제마다 다를 수 있다. 프로그램의 동작이 스레드 스케줄러에 기댄다면 운영체제의 스케줄링 정책에 영향을 받게 되는것이다. 즉, '스레드 스케줄러에 기대는 프로그램은 정확성이나 성능이 운영 체제에 따라 달라지게 된다' 는 점에서 프로그램을 다른 플랫폼에 이식하기 어렵게 된다.

 


3. 스레드 스케줄러에 영향을 받지 않으려면 어떻게하나요?

 

1) 실행 가능한 스레드의 수를 지나치게 만들지 않기

 

실행 가능한 스레드
실행 가능한 스레드는 스레드 큐에 위치하여 언제든 CPU의 선택을 받을 수 있는 스레드를 말한다.

 

 실행 가능한 스레드의 평균적인 수를 프로세서 수보다 지나치게 많아지지 않도록 하는 것이다. 프로세서는 CPU인데, 멀티쓰레드를 지원하는 JVM 에서 스레드 수를 프로세서 수보다 지나치게 많아지지 않도록 하라는 게 무슨 의미인지 이해되지 않았다. 예를들어 필자의 컴퓨터는 멀티코어로 두개의 CPU가 있다. 그럼 스레드 수를 2개보다 지나치게 많아지지 않도록해라? 아무리 생각해도 100개의 스레드는 지나치게 많은 개수같은데, 일반적으로 JVM 스레드가 100개를 넘는 일은 허다하지 않나라는 의문이 들었다.

 

 위 내용에 대해 고민을 많이 했는데, 책을 다시 보니 필자가 핀트를 잘못 짚은것이었다. 실행 가능한 스레드의 수가 많으면 좋지 않다는게 아니라 단순히 스레드 스케줄러의 영향을 받지 않는 방법을 명시한 것이었다.  스레드 개수가 적어 스케줄러가 고민할 거리가 줄어든다면 정책의 영향을 덜받지 않겠는가. 당연한 얘기였다.

 

 다시 돌아가 필자의 컴퓨터는 한 코어에 6개의 스레드를 동작시킬 수 있다. 멀티 코어이니 최대 12개의 스레드를 동시처리 할 수 있다. 이보다 지나치게 많아졌을 때 스레드 스케줄러에 의해 스레드들이 제어될것이고, 이 프로그램이 '스레드 스케줄러에 기대도록' 구현이 되어있다면 문제가 생길 수 있다는 것이 포인트였다.

 

2) 스레드는 맡은 작업을 완료할 때까지 계속 실행되도록 하자

 스레드가 작업을 완료하기 전에 대기 상태로 가는 코드가 없도록 코드를 작성해야한다. 대기한다는 뜻은 곧 스레드 스케줄러의 정책에 의해 제어된다는 의미이기 때문이다. 또한 스레드는 당장 처리해야할 작업이 없다면 실행되게 해서도 안된다.

 


4. 당장 처리해야할 작업이 없다면 실행되게 해선 안된다?

 당장 처리할 작업이 없다면 대기 상태에 들어갈것이다. 그리고 다시 실행되기 위해 지속적인 상태체크를 한다. 즉, 스레드가 바쁜 대기(busy waiting) 상태가 되는것이다.

 당장 처리해야할 작업이 없지만 상태를 검사하기 위해 스레드가 실행된다. 의미없이 CPU를 선점하고 있는 격이기에 의미있는 작업을 맡은 스레드들이 당장 실행될 기회를 박탈당하게 된다.

 


5. Busy Waiting 예시

CountDownLatch 클래스의 기능을 가진 커스텀 클래스를 만들었다. Busy Waiting 상태가 나타나는 코드로CountDownLatch 클래스를 삐딱하게 구현한 코드이다.

 

await() 메서드 내 while(true)에 의해 해당 스레드는 계속해서 CPU를 점유할것으로 보이지만, 실제로는 스케줄링 정책에 대기/실행 상태를 왔다갔다하며 다른 스레드들도 중간 중간 실행될것이다. 그런데 이 메서드가 하는 일이 뭔가? 그냥 대기하는것이다. while 문에 의해 count 값만 계속 체크하는 로직인데, countDown()을 수행하는 스레드와 바쁘게 경쟁하게 된다. 경쟁자가 많다보니 컨텍스트 스위칭이 빈번하게 일어날테고, countDown() 호출이 늦어지는 결과를 초래하게 된다.

 

 이런 프로그램이 프로그램의 동작을 스레드 스케줄러에게 기댄 코드라고 할 수 있다.

 

1) SlowCountDownLatch.java

public class SlowCountDownLatch {

    private int count;

    public SlowCountDownLatch(int count){
        if(count < 0){
            throw new IllegalArgumentException(count + " < 0");
        }
        this.count = count;
    }

    public void await(){
        while(true){
            synchronized (this){
                if (count == 0)
                    return;
            }
        }
    }

    public synchronized void countDown(){
        if (count != 0){
            count --;
        }
    }
}

 

1) Main.java

public static void main(String[] args) {

    SlowCountDownLatch ready = new SlowCountDownLatch(1000);
    SlowCountDownLatch start = new SlowCountDownLatch(1);
    SlowCountDownLatch end = new SlowCountDownLatch(1000);
    ExecutorService executorService = Executors.newFixedThreadPool(1000);

    for(int i =0; i< 1000; i++){
        executorService.submit(() -> {
            ready.countDown();

            start.await();
            System.out.println("끼요옥");

            end.countDown();
        });
    }
    ready.await();
    System.out.println("준비완료. 시작!");
    start.countDown();
    long StartNanos = System.nanoTime();
    end.await();
    System.out.println("걸린시간 : " + ( System.nanoTime() - StartNanos));
}

 

스레드 1000개를 생성하여 SlowCountDownLatch를 테스트한 결과, Java에서 제공하는 CountDownLatch보다 2, 3배 느린 것을 확인할 수 있었다. 책에서는 약 10배가 느렸다고 한다.

 

6. 실행을 양보한다면?

그럼 await() 내 상태 체크가 실패했을 경우 현재 스레드를 해제하고 다른 스레드가 작업할 수 있도록 '양보'하는 건 어떨까? 현재는 운영체제의 스레드 스케줄링 정책에 의해 대기상태로 가고 있는데, 당장 스레드 큐로 보내버리는 것이다.

 Thread.yield() 메서드를 이용하면 현재 스레드를 실행 가능한 상태로 보낼 수 있다.

public void await(){
    while(true){
        synchronized (this){
            if (count == 0)
                return;
            else{
                Thread.yield();
            }
        }
    }
}

 

 여기서 중요한 점은 Thread.yield()는 무작정 양보 되는 것이 아니라, 운영체제에게 '요청'하는 메서드이다. 운영체제의 스레드 스케줄러가 이를 무시할 수 있다. 거절당할 수 있는 것이다. 또한, 운영체제마다 yield()의 동작이 달라질 수도 있다는 점에서 OS에 대한 이식성이 좋지 않으며. 호출이 너무 많아지면 컨텍스트 스위칭이 많아져 성능 저하를 초래할 수 있다. 좋지 않은 방법이다.

 

 스레드의 우선순위를 조절하는 것도 방법인데, 이것도 OS에 대한 이식성이 좋지 않다. 특히 심각한 문제를 스레드 우선순위로 해결하려는 시도는 합리적이지 않다.

 


7. 정리

 프로그램의 동작을 스레드 스케줄러에 기대지 말자. 성능과 속도, 이식성을 모두 해치는 행위이다. 같은 이유로 Thread.yield() 와 스레드 우선순위에 의존해서도 안된다.

 스레드 우선순위는 이미 잘 동작하는 프로그램의 서비스 품질을 높이기 위해 드물게 쓰일 수는 있지만, 간신히 동작하는 프로그램을 '고치는 용도'로 사용해서는 안된다.

반응형
반응형

1.1. 개요

 스레드 안전성 수준을 명시해야하는 이유와, 스레드 안전성 수준에 대해 알아보자.

 

1.2. 멀티 스레드 환경에서의 메서드 호출

 멀티 스레드 환경에서 메서드를 호출했을 때, 메서드 내에서 사용하는 변수 및 인스턴스들의 동기화가 필요한지 알아야한다. 이에 따라 클라이언트에서 이 메서드를 사용하는 방식이 달라지기 때문이다.

 여러 스레드가 동시에 호출할 때 메서드가 어떻게 동작하느냐는 해당 클래스와 이를 사용하는 클라이언트 사이의 중요한 계약 사항 중 하나라고 할 수 있다.

 만약 이러한 내용이 없을 경우 클라이언트는 나름의 가정과 함께 사용할것인데, 이 과정에서 심각한 오류를 발생시킬 수 있다.

 

1.3. synchronized == 스레드 안전하다?

 API 문서에 synchronized 한정자가 붙어 있다고 모두 스레드 안전하다고 할 수 없다. 한정자를 스레드 안전한 API 동작을 목적으로 사용했을 수도 있지만 단순 구현 이슈로 인해 사용했을 수도 있기 때문이다. "synchronized == 멀티 스레드 안전" 이라고 단정짓는 것은 위험하다.

 

1.4. 스레드 안전에 대한 명시

 어떤 메서드를 구현했을 때 스레드 안전성 여부를 명시해야한다. 하지만 내부 구현에 따라 멀티 스레드 환경에서 무조건 안전할수도, 조건부 안전할수도, 클라이언트가 동기화 메커니즘을 적용했을 때 안전할 수도, 무슨 짓을 해도 안전하지 않을수도 있다. 결국 안전성 수준을 명시해야 한다.

 

2. 스레드 안전성 수준

2.1. 불변(immutable) - @Immutable

 클래스의 인스턴스가 마치 상수와 같아서 외부 동기화가 필요없는 수준이다. 대표적으로 String, Long이 있다.

 

2.2. 무조건적 스레드 안전(unconditionally thread-safe) - @ThreadSafe

 클래스의 인스턴스는 수정될 수 있으나, 내부에서 충실히 동기화하여 별도의 외부 동기화가 없이 동시에 사용해도 안전한 수준이다. 대표적으로 AtomicLong, ConcurrentHashMap이 있다.

 

*  ConcurrentHashMap

HashMap에서 세분화된 락을 제공하는 Map 자료형을 말한다. 전체 맵을 잠그는 대신, 내부 데이터를 여러 버킷으로 나누고 개별적인 락을 적용한다. 이를 통해 여러 스레드에서 동시에 다른 버킷에 접근 가능하다.

읽기 작업은 락 없이 수행하며, compute(), merge()와 같은 원자적 연산도 제공한다.

이를 잘 활용하면 멀티 스레드에서 정합성을 유지할 수 있다.

 

2.3. 조건부 스레드 안전(conditionally thread-safe) - @ThreadSafe

 무조건적 스레드 안전과 같으나, 일부 메서드는 외부 동기화가 필요한 수준이다. Collections.synchronized 래퍼 메서드가 반환한 컬렉션들이 여기 속한다.

 

* 조건부 스레드 문서화 시에는 어떤 순서로 호출할 때 외부 동기화가  필요한지, 어떤 락을 얻어야 하는지를 알려줘야한다. 예를들어 Collections.synchronizedMap의 API 문서에는 다음과 같이 써 있다.

 

synchronizedMap이 반환한 맵의 컬렉션 뷰를 순회하려면 반드시 그 맵을 락으로 사용해 수동으로 동기화하라.

	Map<K, V> m= Collections.synchronizedMap(new HashMap<>());
        Set<K> s = m.keySet();
        
        synchronized (m){ // s가 아닌 m을 사용해 동기화해야한다
            for (K key : s){
                key.f();
            }
        }

 

2.4. 스레드 안전하지 않음(not thread-safe) - @NotThreadSafe

 클래스의 인스턴스는 수정될 수 있다. 하지만 멀티 쓰레드 환경에서 사용하려면 클라이언트가 외부 동기화 메커니즘을 사용해야한다. 대표적으로 ArrayList, HashMap이 있다.

 

2.5. 스레드 적대적(thread-hostile)

 외부 동기화로 감싸더라도 안전하지 않은 수준을 말한다.

 

 

3. 정리

클래스의 스레드 안전성은 보통 클래스의 문서화 주석에 기재하지만, Collections.synchronizedMap과 같이 조건을 명시해야할 경우엔 코드 레벨의 주석에 기재하는 것이 좋다.

 추가로 클래스가 외부에서 사용할 수 있는 락을 제공하면 클라이언트에서 일련의 메서드 호출을 원자적으로 수행할 수 있다.

반응형
반응형

1. 개요

 수행하려는 일과 관련 없어 보이는 예외가 튀어나올 수 있을까? 있다. 바로 체인된 메서드가 저수준의 예외를 처리하지 않고 바깥으로 던져버릴때이다.

 

예외를 찾아서...

 

 예외 발생의 원인을 찾기 위해 내부 구현을 모조리 뒤져야한다는 것이다. 자연스레 결합도가 높아질것이다.

 만약 buy() 메서드에서 클라이언트에게 정확한 예외 메시지를 주고싶다는 등의 이유로 이 예외를 잡아 처리한다면 어떨까? 내부 구현을 분석한 후 적절한 예외 메시지를 던져야할것이다. 

 최종적으로 IllegalStatusException에 대한 catch 문을 추가하고 "상품 유효성 검사에 실패했습니다. 관리자에게 문의해주세요" 라는 예외를 던지도록 예외를 전환했다. 하지만 이것도 문제가된다.

 

2. checkStock() 에서 발생한 IllegalStatusException

 며칠 뒤 buy() 메서드 호출과 함께 어디선가 IllegalStatusException이 발생했다. 클라이언트에게는 "상품 유효성 검사에 실패했다"라는 예외 메시지 던져주고 있다. 그런데 확인해보니 이번 예외는 productValidation()가 아닌 checkStock() 에서 발생했고, 재고 관련 인스턴스를 생성할 때 발생했다.

 

재고 관련 문제가 발생했는데 "상품 유효성 검사에 실패했다"는 에러 메시지는 사용자와 개발자 모두를 혼란에 빠지게했다.

결론은 예외를 무작정 던져도 문제이지만, 무작정 던진 걸 상위 수준에서 무작정 catch 해서 처리하는 것도 문제라는 것이다.

 

자, 그럼 어떻게 했어야할까? productValidation()에서 예외를 잡아 본인의 추상화 수준에 맞는 예외로 전환해서 던졌어야한다. ProductValidationException과 같이 말이다. 예외를 밖으로 던지는 것 자체가 책임을 전가하는 것이기에 결합도 측면에서도 좋지 않다.

 

try{
    // 제품 유효성 체크 로직
}catch(IllegalStatusException e){
    throw new ProductValidationException("제품 유효성 체크에 실패하였습니다. 관리자에게 문의해주세요!")
}

 

이처럼 자신의 추상화 수준에 맞는 예외 클래스로 바꿔 던지는 것을 예외 번역(exception translation) 또는 예외 전환 이라고 한다.

 

3.  예외 체이닝

예외를 전환하려 했더니, 저수준 예외가 디버깅에 도움이 될것같아 예외를 살리고 싶다면 어떨까? 문제의 근본 원인인 저수준 예외를 고수준 예외에 실어보내는 방법을 사용해야 한다. 이를 예외 체이닝(exception chaining)이라고 한다.

 

try{
    // 제품 유효성 체크 로직
}catch(IllegalStatusException e){
    throw new ProductValidationException(e) // 예외 체이닝
}

 

class ProductValidationException extends Exception{
    ProductValidationException(Throwable cause){
        super(cause);
    }
}

 

대부분의 표준 예외는 예외 체이닝 생성자를 갖추고 있다. 예외 연쇄는 문제의 원인을 프로그램에서 접근(getCause 메서드)할 수 있게 해주며, 원인과 고수준 예외의 스택 추적 정보를 통합해준다.

 

정리

아래 계층의 예외를 예방하거나 스스로 처리할 수 없고, 그 예외를 상위 계층에 그대로 노출하기 곤란하다면 예외 전환을 사용하자. 무턱대고 예외를 전파하는 것보다 예외 전환이 우수한 방법이다. 하지만 남용해서는 안된다. 가능하다면 저수준 메서드가 반드시 성공하도록 해야한다. 예를들어 전달하는 하위 메서드에 매개변수를 전달하기 전에 매개변수에 대한 체크를 하는 방법이 있을것이다.

반응형
반응형

1. NAT Gateway란?

 네트워크 주소 변환 서비스를 말하며, 프라이빗 서브넷의 인스턴스이 외부 인터넷망으로 나갈 수 있도록 함과 동시에 Public IP를 부여하는 역할을 한다.

 

NAT (Network Address Translation)
 네트워크 주소 변환의 줄임말인 NAT는 IP패킷의 TCP/UDP 포트 숫자와 출발지 및 목적지의 IP 주소 등을 재기록하면서 라우터를 통해 네트워크 트래픽을 주고 받는 기술을 뜻한다. IP 를 변환하겠다는 뜻이다.
 NAT를 이용하는 이유는 대개 사설 private Network에 속한 여러개의 호스트가 하나의 공인 IP를 사용하여 인터넷에 접속하기 위함이다.

 


 

2. 인터넷 게이트웨이랑 똑같은데요??

 인터넷 게이트웨이는 서브넷을 인터넷 망과 직접적으로 연결시켜 외부로 나갈수도, 외부에서 접근할 수도 있는 구조이다. 이에 반에 NAT GateWay는 서브넷 내의 인스턴스가 외부, 즉 인터넷망으로 나갈 순 있지만, 외부에서 내부 즉, 해당 인스턴스로의 접근은 불가능하다. 이유는 외부 인터넷망으로 나갈 때에만 Public IP를 부여하기 때문이다.

 


3. AWS NAT Gateway 설정 방법

 

1) 퍼블릭 서브넷에서 NAT 게이트웨이 생성

 프라이빗 서브넷의 인스턴스는 퍼블릭 서브넷에 위치한 NAT 게이트웨이를 통해 인터넷에 연결할 수 있다. 퍼블릭 서브넷에서 NAT 게이트웨이를 생성하자. 인터넷 망으로 나갈 때 설정할 공인 IP가 필요하기에 탄력적 IP 를 새로 할당하거나, 기존에 있는 탄력적 IP를 사용해야한다.

NAT 게이트웨이 생성

 

2) 프라이빗 서브넷 라우팅 테이블에 NAT Gateway 추가

 NAT Gateway 생성이 완료되면, 프라이빗 서브넷의 라우팅 테이블에 NAT Gateway를 추가해주자. 외부로 나갈 때 NAT Gateway로 라우팅되어야 하므로 대상을 0.0.0.0/0으로 설정한다.

NAT Gateway 라우팅

 

3) 테스트

 이제 프라이빗 서브넷의 인스턴스에 SSH 접속 후 아래 명령어를 입력하여 외부와 통신이 되는지 확인하자.

curl https://tlatmsrud.tistory.com

 

통신이 된다면 NAT Gateway를 통해 외부로 나간것임을 알 수 있다.

 

반응형
반응형

1. 개요

 비지니스 코드를 재사용 하는 것이 좋은 것처럼, 예외도 재사용 하는 것이 좋다. 자바 라이브러리는 대부분 API에서 쓰기 충분한 수의 예외를 제공한다. 커스텀 예외를 사용해도 되지만 표준 예외를 사용했을 때 사용자 입장에서 익숙한 예외가 발생하므로 예외 상황 파악과 코드 이해가 쉬워진다.

 

 재사용할만한 표준 예외들을 알아보자.

 

2. 재사용할만한 예외

 

IllegalArgumentException

 호출자가 인수로 부적절한 값을 넘길 때 던지는 예외이다. 예를 들어 반복 횟수를 지정하는 매개변수에 음수를 건넬 때 쓸 수 있다.

 

public class Call {

    private static final String HI = "hello";

    public void sayHi(int cnt){

        if(cnt < 0){
            throw new IllegalArgumentException("cnt 값은 0보다 커야 합니다.");
        }

        for(int i =0; i<cnt; i++){
            System.out.println(HI);
        }
    }
}

 

 

IllegalArgumentException

 

IllegalStateException

 대상 객체의 상태가 호출된 메서드를 수행하기에 적합하지 않을 때 주로 던진다. 예를 들어 제대로 초기화되지 않은 객체를 사용하려 할 때 던질 수 있다.

 

public class Toy {

    private String weapon;

    public Toy(String weapon){
        this.weapon = weapon;
    }

    public void attack(){
        if(weapon == null){
            throw new IllegalStateException("weapon 값이 초기화되지 않았습니다. 초기화시켜주세요.");
        }

        System.out.println(weapon +" 공격!");
    }
}

 

IllegalStateException

 

NullPointerException

 일반적으로 null 값을 갖고 있는 객체나 변수를 호출할 때 발생하지만, 특정 상황에선 이를 재사용할 수 있다. null 값을 허용하지 않는 메서드에 null을 건네면 관례상 IllegalArgumentException이 아닌 NullPointerException을 던진다.

 아래와 같이 attack() 메서드를 실행하기 전, 생성자 메서드 레벨에서 NullPointerException을 사용하는 방법이다. 인스턴스 생성 시점에 멤버필드에 대한 유효성이 보장된다는 점에서 더 좋은 방법이라 생각한다.

 

public class Toy {

    private String weapon;

    public Toy(String weapon){
        if(weapon == null){
            throw new NullPointerException("weapon 을 null 로 설정할 수 없습니다.");
        }
        this.weapon = weapon;
    }

    public void attack(){
        System.out.println(weapon +" 공격!");
    }
}

NullPointerException

 

UnsupportedOperationException

 클라이언트가 요청한 동작을 대상 객체가 지원하지 않을 때 던진다. 보통 구현하려는 인터페이스의 메서드 일부를 구현할 수 없을 때 쓰는데, 예를 들어 원소를 넣을 수만 있는 List 구현체에 누군가 remove 메서드를 호출하면 이 예외를 던질 것이다.

 

public class NoRemoveList<E> extends ArrayList<E> {

    @Override
    public boolean remove(Object o) {
        throw new UnsupportedOperationException("remove는 지원하지 않습니다.");
    }
}

 

UnsupportedOperationException

 

이 외에도 어떤 시퀀스의 허용 범위를 넘을 때 사용하는 IndexOutOfBoundException, 단일 스레드에서 사용하려고 설계한 객체를 여러 스레드가 동시에 수정하려 할 때 사용하는 ConcurrentModificationException 등이 있다.

 

3. 재사용하지 말아야할 예외

 Exception, RuntimeException, Throwable, Error는 재사용하지 말자. 이 예외들은 다른 예외들의 상위 클래스이기 때문에 예외 상황에 대한 정보를 명확하게 전달하기 어렵다.

 

4. 정리

 상황에 부합한다면 항상 표준 예외를 재사용하자. 단, 표준 예외를 사용하기 전 API 문서를 참고해 그 예외가 어떤 상황에서 던져지는지 꼭 확인해야 한다. 예외의 이름 뿐 아니라 예외가 던져지는 맥락도 부합할 때만 재사용한다. 더 많은 정보를 제공하길 원한다면 표준 예외를 확장해도 좋다.

 

반응형
반응형

서브넷이 뭔가요?

서브넷은 서브 네트워크를 말하며, 기존 네트워크 영역을 분할해 더 작은 크기의 네트워크 영역으로 쪼갠 네트워크이다. (말 그대로 서브 네트워크...) AWS에서의 서브넷은 VPC 내에 생성하는데, VPC 가 가진 CIDR 블록(기존 네트워크 영역) 내에서 더 작은 CIDR 블록(더 작은 크기의 네트워크 영역) 로 쪼갠 네트워크 영역을 말한다.

 

아래와 같이 VPC를 10.0.0.0/16 으로 CIDR 대역을 설정한 후 10.0.10.0/25, 10.0.20.0/25 CIDR 블럭을 갖는서브넷을 각각 생성하였다. 즉, A 서브넷은 10.0.10.0 ... 10.0.10.127 IP 대역을, B 서브넷은 10.0.20.0 ... 10.0.20.127의 IP 대역을 갖게 되는것이다.

서브넷팅

 


퍼블릭 서브넷, 프라이빗 서브넷??

서브넷 유형을 말하며, 이 두 유형은 서브넷을 생성할 때 지정하는게 아니라 서브넷에 할당된 라우팅 테이블에 의해 지정된다. 퍼블릭의 의미는 인터넷 망, 정확히 말하면 인터넷 게이트웨이와 직접 연결되어 있는 것을 말한다.

 

다시말하면 퍼블릭 서브넷은 라우터에 의해 인터넷 게이트웨이와 직접 연결되는 서브넷이고, 프라이빗 서브넷은 인터넷 게이트웨이와 직접 연결되지 않은 서브넷이다. 라우팅 테이블 설정에 따라 퍼블릿 서브넷이 프라이빗 서브넷이 될수도, 그 반대가 될수도 있다.

 

퍼블릭 서브넷과 브라이빗 서브넷

 

 


인터넷 게이트웨이가 뭔가요?

인터넷 게이트웨이란 VPC와 인터넷 간에 통신할 수 있게 해주는 VPC 구성요소를 말한다. 줄여서 IGW라고 부른다.

 


라우팅 테이블에 IGW만 추가하면 외부와 통신이 가능한건가요?

IGW에 대한 라우팅 정보를 추가하기만 하면 해당 서브넷 내 리소스들이 인터넷 망으로 나갈 수 있을까? 그렇지 않다. 서브넷 내에 퍼블릭 IP를 가진 리소스가 존재하지 않기 때문이다.

 인터넷 게이트웨이를 설정했다면, NAT 게이트웨이를 설정하거나, 서브넷 상의 리소스에 퍼블릭 IP를 할당해야만이 인터넷 게이트웨이를 통해 인터넷에 연결할 수 있다.

반응형
반응형

AWS VPC 설명서 기반으로 스터디한 내용입니다.

https://docs.aws.amazon.com/ko_kr/vpc/latest/userguide/what-is-amazon-vpc.html

 

Amazon VPC란 무엇인가? - Amazon Virtual Private Cloud

이 페이지에 작업이 필요하다는 점을 알려 주셔서 감사합니다. 실망시켜 드려 죄송합니다. 잠깐 시간을 내어 설명서를 향상시킬 수 있는 방법에 대해 말씀해 주십시오.

docs.aws.amazon.com

 

개요

 예전 AWS 로 인프라를 구성한 적이 있는데, 구성 당시에는 누군가 만들어놓은 메뉴얼을 따라하기만 했지, 각각의 리소스들이 왜 필요한지, AWS 내에서 어떤 역할을 하는지에대한 고민은 하지 않았다. 그래서 이번 기회에 AWS 구조와 용어에 대해 이해하고 싶었고, 가장 기본인 VPC부터 공부하게 되었다.

 


Amazon VPC가 뭔가요?

아마존(Amazon) 에서 제공하는 격리된(Private) 가상(Virtual) 클라우드(Cloud) 서비스를 말한다. 조금 풀어 말하면, 아마존과 같은 퍼블릭 클라우드 환경 일부분을 고객 전용으로 프라이핏하게 사용할 있는 가상 네트워크 말한다. 가상 네트워크라는 말이 알쏭달쏭한데, 글을 읽다보면 이해할 수 있을것이다.

 

"VPC는 리전의 모든 가용 영역에 적용됩니다." ??

Amazon VPC 사용 설명서의 첫 시작 문구이다. 첫 시작부터 발걸음을 돌리고 싶게 만드는 알쏭달쏭한 문구인데, 첫 문구인만큼 이 의미를 이해하는 것이 중요하다고 생각했다. 먼저 리전과 가용 영역에 대해 알아보자.

 


리전(Region)과 가용영역(Availability Zone)

리전 (Region)

리전이란 전 세계에서 데이터 센터를 클리스터링하는 지리적 위치를 말한다. 여기서 지리적 위치라고 표현한 이유는 실제 물리적인 위치보다 더 넓은 범위를 표현하는 용어이기 때문이다. 누군가 "너 어디살아?" 라고 물었을때 "나 서울시 xx구 xx아파트 203동 101호에 살아" 라고 하지 않고 "나 서울살아" 라고 하는것과 같다고 생각하면 된다.

 

데이터 센터
용어만 들으면 데이터가 저장된 센터? DB를 말하는건가? 라고 착각이 들 수 있다. 클라우드 컴퓨팅에서의 데이터 센터란 컴퓨팅 시스템 및 하드웨어 장비가 저장된 물리적 위치를 말한다. 즉, 클라우딩 관련 장비가 저장된 센터를 말한다.

 

Region

 

 

위 그림을 보면 알 수 있듯이 클라우드 서비스를 제공하기 위한 데이터 센터의 지리적 위치는 미국 동부, 서부, 서울, 도쿄 등 지역별로 고루 퍼져있다.

 

가용영역 (Availability Zone)

 리전별로 하나의 데이터 센터만을 운용하고 있을까? 아니다. 최소 2개 이상의 데이터 센터를 가져야 리전으로써의 조건이 성립된다. 즉, 서울 리전에는 실제로 2개 이상의 데이터 센터들이 어딘가에서 운용되고 있는 것이다. 그리고 이렇게 퍼져있는 데이터 센터들을 논리적으로 묶어놓은 것을 가용영역이라 한다.

 

Region - AZ - IDC

 

위 그림은 리전과, 가용영역, 그리고 실제 데이터 센터를 도식화 한것이다.

 


 

"VPC는 리전의 모든 가용 영역에 적용됩니다." !!

다시 돌아가서 이 문구의 의미는 뭔지 생각해보자. 해석하면 "VPC라는 가상 네트워크는 리전 내에 있는 모든 가용영역에 위치시킬 수 있다는 뜻이고 이 말은 하나의 VPC 내에 생성되는 리소스들을 여러 데이터 센터에 위치시킬 수 있다는 뜻이다."

아래 그림과 같이 말이다. VPC가 가상의 네트워크이기 때문에 여러 가용영역을 공유 사용할 수 있는것이다.

 

여러 AZ에 접근가능한 VPC

 

 

VPC 에는 자체 IP가 없고 IP 대역(CIDR) 을 설정하던데...

 VPC를 생성한다고 해서 VPC 자체에 대한 IP가 할당되지 않는다. 말 그대로 "가상의 네트워크"이기 때문이다. 대신 VPC 내에 실제 리소스를 생성할 때에는 해당 리소스에 IP가 할당되는데, 이때 할당되는 IP의 범위, 즉 CIDR를 VPC 생성 시 설정해야 한다. VPC를 생성할 때 IP 프로토콜과 CIDR를 설정하는 이유가 바로 이것이다.

 

VPC 설정의 CIDR 블록 설정부분

 

IPv4 프로토콜을 사용할 경우 CIDR 블록 크기 설정 시 RFC1918 규격과 AWS 자체 VPC CIDR 블럭 규칙에 따라 CIDR를 설정해야 한다. 이를 준수하는 CIDR 블록 크기는 아래로 한정된다. 

 

10.0.0.0/16 ~ /28

172.16.0.0/16 ~ /28

192.168.0.0/16 ~ /28

 

 

RFC 1918
프라이빗 IP의 국제 규격으로 아래 대역 범위를 갖는다.

RFC 1918

 


 

달랑 VPC 만 생성해주지 않아요~

VPC를 생성하면 VPC만 생성되는게 아니라 기본 리소스인 기본 DHCP 옵션 세트, 기본 네트워크 ACL, 기본 보안그룹, 기본 라우팅 테이블도 함께 생성된다. 각각에 대해 알아보자.

 

첫째, 기본 DHCP 옵션 세트

 

DHCP가 뭔가요?

 Dynamic Host Configuration Protocol의 약자로 네트워크에 위치한 컴퓨터 및 기타 장치에 IP 주소와 같은 네트워크 정보를 자동으로 할당하기 위한 프로토콜을 말한다. 네트워크 설정을 DHCP 서버가 중앙 집중식으로 관리하는 클라이언트/서버 모델인 것이다.

 이전에는 네트워크에 위치한 컴퓨터 및 기타 장치의 IP 주소를 수동으로 할당했지만, 오늘날에는 DHCP를 사용해 동적으로 할당하고 있다. 생소하게 느껴질 수 있지만 사실 대부분의 컴퓨터 사용자들이 이 프로토콜을 사용하여 네트워크 설정을 자동화했을 것이다. 필자의 맥북또한 마찬가지인데, 네트워크 탭의 IPv4의 구성 방식이 DHCP를 사용하도록 설정되어 있어 IP, DNS서버, 게이트웨이 주소 등을 따로 설정하지 않아도 자동으로 할당되는 것을 확인할 수 있다.

필자의 맥북 네트워크 설정

 

 

DHCP를 사용하면 뭐가 좋나요?

네트워크 설정을 자동화할 수 있고, 수동 IP 할당 시 발생할 수 있는 IP 충돌 문제를 예방할 수 있다.

 

DHCP 옵션 세트가 뭔가요?

EC2 인스턴스가 실행될 때 DHCP를 통해 자동으로 네트워크 설정이 되도록 DHCP 서버로 요청하는데, 이 DHCP 관련 설정이 담긴 세트이다. 각 리전마다 각기 다른 기본 DHCP 옵션 세트를 갖고 있다.

 

DHCP 옵션 세트가 AWS에서 어떻게 쓰이는지 알려주세요

VPC 내 EC2와 같은 리소스가 실행되면 IP, DNS 서버와 같은 네트워크 설정을 위해 Amazon DHCP 서버로 요청하게 된다. 그럼 DHCP 서버는 VPC 내 설정된 DHCP 옵션 세트를 로드하게 되는데, 이 옵션 세트에 따라 IP 주소와 DNS 서버와 같은 네트워크 설정을 해당 리소스에 할당하게 된다.

 

 참고로 리소스에 네트워크 설정이 정상적으로 할당된 경우 해당 리소스는 자신에게 할당된 IP 정보를 자동으로 라우팅 테이블에 등록하게 되는데, 이러한 과정으로 인해 내부 리소스간의 네트워킹이 가능한 것이다.

 

AWS에서의 DHCP 옵셧 세트 사용 방식

 

그래서 기본 DHCP 옵션 세트는 뭐라고요?

기본 DHCP 옵션 세트란 리소스의 네트워킹 설정을 위해 DHCP 서버가 참조하는 기본옵션들을 말한다. VPC 라는 가상 네트워크 환경에 설정한 CIDR 대역에 맞는 IP로 할당되어야 하지 않겠는가? 이러한 외부 정보가 없다면 어떤 IP를 할당해야하는지에 대한 기준이 잡히지 않을 것이다. (이건 필자의 지극히 주관적인 생각입니다.)

 


둘째, 기본 네트워크 ACL

 

네트워크 ACL이 뭔가요?

네트워크 ACL이란 Network Access Control List의 약자로 '서브넷 수준'에서 특정 인바운드 또는 아웃바운드 트래픽에 대한 접근 제어 리스트를 말한다. 아래는 서브넷이 2개인 VPC 내에서 네트워크 ACL의 역할을 알려주는 그림이다.

 

네트워크 ACL

 

트래픽이 VPC로 들어오면 라우터에서 라우팅 테이블을 확인해 트래픽을 타겟으로 보낸다. 이때 네트워크 ACL로 하여금 해당 트래픽이 서브넷으로 들어가고 나갈 수 있는지를 제어하는 것이다. 네트워크 레벨에서의 방화벽인 셈이다.

 

그래서 기본 네트워크 ACL은요?

AWS에서 기본으로 제공하는 네트워크 ACL로, VPC 내 서브넷을 생성할 경우 네트워크 ACL을 설정해야 하는데, 설정하지 않을 경우 자동으로 할당되는 네트워크 ACL이다. 기본 네트워크 ACL의 정책은 모든 인바운드 및 아운바운드에 대한 IPv4, IPv6 트래픽을 허용한다.

기본 네트워크 ACL의 아웃바운드 규칙
기본 네트워크 ACL의 인바운드 규칙

 


셋째, 기본 보안그룹

 

보안그룹이 뭔가요?

보안 그룹은 VPC 내 리소스에 대한 접근을 제어하는 그룹을 말한다. 예를 들어 특정 IP 에 대한 인바운드를 차단하는 보안 그룹을 만들고, 2개의 EC2 인스턴스의 보안 그룹에 이를 적용한다면, 특정 IP가 두 EC2 인스턴스로 접근하지 못하도록 차단한다.

 

네트워크 ACL과 같은거 아닌가요?

인바운드, 아운바운드에 대한 접근을 제어한다는 점에서 비슷하지만, 적용 레벨이 다르다. 네트워크 ACL은 서브넷 레벨에서, 보안그룹은 리소스 레벨에 적용된다.

 

그래서 기본 보안그룹은요?

VPC를 생성할 경우 기본으로 제공되는 보안그룹이다. 모든 트래픽에 대해 인바운드와 아웃바운드를 허용하도록 정책이 설정되어 있다

 

보안그룹

 

위 그림은 VPC 내 위치한 두 개의 EC2가 기본 보안 그룹을 적용한 상황이다. 기본 보안그룹 설정했으니 모든 포트 및 IP로부터 오는 트래픽을 받을 수 있게 된다. 단, 기본 보안그룹은 인터넷 게이트웨이 또는 NAT 게이트웨이로부터 오는 트래픽을 거부하도록 설정되어 있다. 만약 NAT 게이트웨이나 인터넷 게이트웨이를 사용한다면 커스텀 시큐리티 그룹을 만들어 각 인스턴스에 적용하면 된다.

 


기본 라우팅 테이블

 

라우터부터 알고가자

라우터란 네트워크 데이터 전송을 위해 최적 경로를 선택한 후 네트워크간 통신 있도록 도와주는 인터넷 장비이다.

그렇다면 라우터는 어떻게 '최적의 경로' 찾아낼 수 있는걸까? 그건 바로 라우터가 가진 '라우팅 테이블' 참고했기 때문이다.

 

라우팅 테이블이 뭐에요?

라우팅 테이블이란 네트워크에서 목적지 주소를 통해 물리적 목적지에 도달하기 위한 경로들이 저장된 테이블이다. 라우터로 하여금 최적의 경로를 선택하도록 도와주는 역할을 한다.  라우터가 가진 라우팅 테이블은 목적지에 도달하기 위해 거쳐야할 다음 라우터의 정보를 가지고 있다.

 

라우팅 테이블은 누가 작성하는 건가요?

관리자가 수동으로 작성할 수도 있지만, 일반적으로 라우팅 프로토콜을 통해 자동으로 라우팅 테이블이 만들어진다. 라우팅 프로토콜(Routing Protocol)이란 라우터끼리 경로 정보를 교환하는 프로토콜로 RIP, BGP, OSPF 프로토콜이 있다.

 

그래서 기본 라우팅 테이블은요?

VPC 만들면 기본으로 적용되는 라우팅 테이블이다. 서브넷을 생성할 라우팅 테이블을 명시적으로 할당해야하는데, 이를 하지 않을 경우 이 기본 라우팅 테이블이 서브넷에 할당되게 된다.

 기본 라우팅 테이블에는 로컬 라우팅만 포함된다. 그런데 VPC 내에 NAT 게이트웨이를 생성하면 VPC 기본 라우팅 테이블에 0.0.0.0/0 트래픽에 대한 타겟 경로를 NAT 게이트웨이로 자동 추가한다. 외부로 나갈때 NAT 게이트웨이를 통해 자동으로 IP가 변경하도록 하기 위함이다. 이를 암시적 서브넷 연결이라고 한다.

 

NAT
Network Address Translation(네트워크 주소 변환)의 약자로 네트워크 주소인 IP를 변환하는 용어이다.
NAT 게이트웨이
IP를 변환시켜주는 장비를 말하며, 일반적으로 사설 네트워크에 속한 호스트가 하나의 공인 IP 주소를 사용하여 인터넷망에 접속하기 위해 사용한다.

 

 이 라우팅 테이블에 설정된 타겟에 따라 서브넷의 유형이 결정된다. 서브넷에 할당된 라우팅 테이블에 인터넷 게이트웨이가 추가되어 있을 경우 퍼블릿 서브넷, 인터넷 게이트웨이가 없다면 프라이빗 서브넷으로 구분된다. 이 내용은 아래 게시글을 참고해보면 좋다.

https://tlatmsrud.tistory.com/172#comment14007882

 

[AWS] 서브넷이란? / 퍼블릿 서브넷 / 프라이빗 서브넷

서브넷이 뭔가요?서브넷은 서브 네트워크를 말하며, 기존 네트워크 영역을 분할해 더 작은 크기의 네트워크 영역으로 쪼갠 네트워크이다. (말 그대로 서브 네트워크...) AWS에서의 서브넷은 VPC

tlatmsrud.tistory.com

 

반응형

+ Recent posts