아이템 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라

사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않다.

대신 클래스가 여러 자원 인스턴스를 지원해야 하며, 클라이언트가 원하는 자원을 사용해야 한다.

--> 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식으로 하면 된다. (의존 객체 주입 패턴)

·  의존 객체 주입 패턴의 장점:

  1. 자원이 몇 개든 의존 관계가 어떻든 상관없이 잘 동작한다.

  2. 불변(아이템17)을 보장하여 여러 클라이언트가 의존 객체를 안심하고 공유할 수 있도록 한다.

이 패턴의 쓸만한 변형으로, 생성자에 자원 팩터리를 넘겨주는 방식이 있다. 즉, 팩터리 메서드 패턴(Gamma95)을 구현하는 것이다.

팩터리: 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체

 

아이템 6. 불필요한 객체 생성을 피하라

똑같은 기능의 객체를 매번 생성하기보다는 객체 하나를 재사용하는 편이 나을 때가 많다.

·  생성자 대신 정적 팩터리 매서드를 제공하는 불변 클래스에서는 불필요한 객체 생성을 피할 수 있다.

ex) Boolean(String) 생성자 대신 Boolean.valueOf(String) 팩터리 메서드를 사용하는 것이 좋다.

생성 비용이 아주 비싼 객체가 반복해서 필요하다면 캐싱하여 재사용하는 것이 낫다.

불필요한 객체를 만들어내는 예로 오토박싱(auto boxing)을 들 수 있다.

오토박싱은 프로그래머가 기본 타입과 박싱된 기본 타입을 섞어 쓸 떄 자동으로 상호 변환해주는 기술이다.

오토박싱은 기본 타입과 그에 대응하는 박싱된 기본 타입의 구분을 흐려주지만, 완전히 없애주는 것은 아니다.

 

아이템 7. 다 쓴 객체 참조를 해제하라

아래 스택 클래스는 메모리 누수가 발생한다.

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    
    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }
    
    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }
    
    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        return elements[--size];
    }
    
    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

- 이 스택을 사용하는 프로그램을 오래 실행하다 보면 점차 가비지 컬렉션 활동과 메모리 사용량이 늘어나 결국 성능이 저하된다.

- 드물긴 하지만 심할 때는 디스크 페이징이나 OutOfMemoryError를 일으켜 프로그램이 예기치 않게 종료된다.

 

· pop 메서드에서 메모리 누수가 발생한다.

- 스택이 커졌다가 줄어들 때, 스택에서 꺼내진 객체들은 프로그램에서 더 이상 사용하지 않더라도 가비지 컬렉터가 회수하지 않는다.

- 꺼내진 객체들이 다 쓴 참조(obsolete reference)를 여전히 가지고 있기 때문이다.

- elements 배열의 '활성 영역'밖의 참조들이 모두 여기에 해당한다. 활성 영역은 인덱스가 size보다 작은 원소들로 구성된다.

- 다쓴 참조: 앞으로 다시 쓰지 않을 참조

 

·  객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐 아니라 그 객체가 참조하는 모든 객체(또 그 객체들이 참조하는 모든 객체)를 회수해가지 못한다.

- 단 몇 개의 객체가 매우 많은 객체를 회수되지 못하게 할 수 있고, 잠재적으로 성능에 악영향을 줄 수 있다.

 

해결 방법

· 참조를 다 썼을 때 null(참조 해제)한다.

public Object pop() {
    if (size == 0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; // 다 쓴 참조 해제
    return result;
}

- null 처리한 참조를 실수로 사용하려 하면 프로그램은 즉시 NullPointerException을 던지며 종료된다.

- null 처리하지 않았다면, 아무 내색 없이 무언가 잘못된 일을 수행할 수 있다.

 

· 객체 참조를 null 처리하는 일은 예외적인 경우여야 한다. 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내는 것이다.

- 변수의 범위를 최소로 되게 정의했다면(아이템 57) 이 일은 자연스럽게 이뤄진다.

- null 처리를 해야하는 경우는 앞선 코드의 스택 처럼 자기 메모리를 직접 관리하는 클래스이다. 비활성 영역에서 참조하는 객체가 더 이상 쓸모없다는 것을 프로그래머만 알지 가비지 컬렉터는 알지 못하기 때문이다. 이럴 때는 null 처리를 하여 가비지 컬렉터에 직접 알려야한다.

 

· 캐시 역시 메모리 누수를 일으키는 주범이다.

- 객체 참조를 다 쓴 뒤 함참을 그냥 놔두는 일을 자주 접할 수 있다.

- 해결 방법1: 외부에서 키를 참조하는 동안만 엔트리가 살아 있는 캐시가 필요하다면, WeakHashMap을 사용해 캐시를 만든다.

다 쓴 엔트리는 즉시 자동으로 제거된다.

- 해결 방법2: 캐시를 만들 때 보통 캐시 엔트리의 유효 기간을 정확히 정의하기 어렵기 때문에 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식이 흔히 사용된다. 이런 방식에서는 쓰지 않는 엔트리를 이따금 청소해야한다. ScheduledThreadPoolExecutor 같은 백그라운드 스레드를 활용하거나 캐시에 새 엔트리를 추가할 때 부수 작업으로 수행할 수 있다.

- LinkedHahshMap은 removeEldestEntry 메서드를 써서 후자의 방식으로 처리한다.

- 더 복잡한 캐시를 만들고 싶다면 java.lang.ref 패키지를 직접 활용해야 한다.

아이템 8. finalizer와 cleaner 사용을 피하라

· 자바는 finalizer, cleaner 즉시 수행된다는 보장이 없다.

· finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요하다.

- 오동작, 낮은 성능, 이식성 문제의 원인이 되기도 한다.

· 자바 9에서 finalizer가 deprecated되고, cleaner를 그 대안으로 소개했지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요하다.

반납할 자원이 있는 클래스는 AutoCloseable을 구현하고 클라이언트에서 close()를 호출하거나 try-with-resource를 사용해야한다.

아이템 1. 생성자 대신 정적 팩터리 메서드를 고려하라

정적 팩터리 메서드의 장점

1. 이름을 가질 수 있다. 

2. 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다. 

3. 반환 타입의 하위 타입 객체를 반환할 수 있다.

4. 입력 매개변수에 따라 매번 다른 객체를 반환할 수 있다.

5. 정적 팩터리 메서드를 작성하는 시점에 반환할 객체의 클래스가 존재하지 않아도 된다.

 

정적 팩터리 메서드의 단점

1. 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.

2. 저정 팩터리 메서드는 프로그래머가 찾기 어렵다.

 

아이넴 2. 생성자에 매개변수가 많다면 빌더를 고려하라

정적 팩터리와 생성자는 선택적 매개변수가 많을 때 적절히 대응하기 어렵다는 제약이 있다.

· 이러한 문제의 대안으로 점층적 생성자 패턴(telescoping constructor pattern)과 자바빈즈 패턴이 등장했지만, 한계가 존재한다.

· 점층적 생성자 패턴의 안전성과 자바빈즈 패턴의 가독성을 겸비한 빌더 패턴으로 이러한 문제를 해결할 수 있다.

- 클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자나 정적 팩터리를 호출해 빌더 객체를 얻고, 빌더 객체가 제공하는 세터 메서드들로 원하는 선택 매개변수를 설정하고, 매개변수가 없는 build 메서드를 호출해 객체를 얻는다.

아이템 3. private 생성자나 열거 타입으로 싱글턴을 보증하라

· 싱글턴이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다.

ex) 함수와 같은 무상태 객체, 설계상 유일해야 하는 시스템 컴포넌트

싱글턴의 문제점

· 클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기 어려워 질 수 있다.

- 타입을 인터페이스로 정의한 후 그 인터페이스를 구현해서 만든 싱글턴이 아니라면 싱글턴 인스턴스를 mock 구현으로 대체할 수 없기 때문이다.

1. public static 멤버가 final 필드인 방식

· 장점: 해당 클래스가 싱글턴임이 API에 명백히 드러난다. public static 필드가 final이니 절대로 다른 객체를 참조할 수 없다. 

· 문제점:

 1. 권한이 있는 클라이언트에서 리플렉션 API인 AccessibleObject.setAccessible을 사용해 private 생성자를 호출 할 수 있다.

 2. 생성되는 시점을 조절할 수 없다. (클래스가 다른 자원(DB커넥션 등)에 의존해야 한다면 이용 불가능)

- 방어 방법: 생성자를 수정하여 두 번째 객체가 생성되려 할 때 예외를 던진다.

2. 정적 팩터리 메서드를 public static 멤버로 제공하는 방식

· 장점:

  1. API를 바꾸지 않고도 싱글턴이 아니게 변꼉할 수 있다. 

  ex) 유일한 인스턴스를 반환하던 팩터리 메서드가 호출하는 스레드별로 다른 인스턴스를 넘겨줄 수 있다.

  2. 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다. (아이템30)

  3.정적 팩터리의 메서드 참조를 공급자(supplier)로  사용할 수 있다.

  ex) Elvis::getInstance를 Supplier<Elvis>로 사용할 수 있다. (아이템 43, 44)

 

· 문제점: 리플렉션을 통한 예외는 똑같이 적용된다.  

 

· 두 방식 모두 생성자는 private로 감추고, 유일한 인스턴스에 접근할 수 있는 수단으로 public static 멤버를 하나 마련한다.

· 둘 중 하나의 방식으로 만든 싱글턴 클래스를 직렬화하려면, Serializable을 구현한다고 선언하는 것만으로는 부족 하다.

- 모든 인스턴스 필드를 transient로 선언하고, readResolve 메서드를 제공해야 한다. (아이템 89)

- 이렇게  하지 않으면, 직렬화된 인스턴스를 역직렬화할 때마다 새로운 인스턴스가 만들어 진다.

3. 원소가 하나인 열거 타입을 선언한다.

· 장점:

  1. 간결하다.

  2. 추가 노력 없이  직렬화할 수 있다.

  3. 아주 복잡한 직렬화 상황이나 리플렉션 공격에도 제2의 인스턴스가 생기는 일을 완벽히 막아준다.

· 대부분의 상황에서 싱글턴을 만드는 가장 좋은 방법이다.

· 문제점: 싱글턴이 Enum 외의 클래스를 상속해야 한다면, 이 방법은 사용할 수 없다.

- 열거 타입이 다른 인터페이스를 구현하도록 선언할 수 없다.

 

아이템 4. 인스턴스를 막으려거든 private 생성자를 사용하라

· 정적 메서드와 정적 필드만 담은 클래스는 객체 지향적으로 사고하지 않는 사람들이 종종 남용하는 방식이지만, 나름의 쓰임새가 있다.

1. java.lang.Math, java.util.Arrays처럼 기본 타입 값이나 배열 관련 메서드들을 모아놓을 수 있다.

2. java.util.Collections처럼 특정 인터페이스를 구현하는 객체를 생성해주는 정적 메서드(혹은 팩터리)를 모아놓을 수 있다.

(자바 8 부터는 이런 메서드를 인터페이스에 넣을 수 있다)

3. final 클래스와 관련된 메서드를 모아 놓을 때 사용한다. final 클래스를 상속해서 하위 클래스에 메서드를 넣는 건 불가기 때문이다.

· 정적 멤버만 담은 유틸리티 클래스는 인스턴스로 만들어 쓰려고 설계한 게 아니다. 따라서 private 생성자를 추가해 클래스의 인스턴스화를 막아서 사용할 수 있다. (생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자를 만든다)

 

아이템 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라

· 많은 클래스가 하나 이상의 자원에 의존한다. 사용하는 자원에 따라 동작이 달라지는 클래스에 경우 클래스가 여러 자원 인스턴스를 지원해야 하며, 클라이언트가 원하는 자원을 사용해야 한다.

·  의존 객체 주입 패턴의 장점:

  1. 자원이 몇 개든 의존 관계가 어떻근 상관없이 잘 동작한다.

  2. 불변(아이템17)을 보장하여 여러 클라이언트가 의존 객체를 안심하고 공유할 수 있도록 한다.

 

· 의존 객체 주입은 생성자, 정적 팩터리 모두에 똑같이 응용할 수 있다.

· 이 패턴의 쓸만한 변형으로, 생성자에 자원 팩터리를 넘겨주는 방식이 있다. 즉, 팩터리 메서드 패턴(Gamma95)을 구현하는 것이다.

- 팩터리: 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체

아이템 6. 불필요한 객체 생성을 피하라

· 똑같은 기능의 객체를 매번 생성하기보다 객체 하나를 재사용하는 편이 나을 때가 많다. 재사용은 빠르고 세련되다. 특히 불변 객체(아이템 17)는 언제든 재사용할 수 있다.

 

·  생성자 대신 정적 팩터리 매서드를 제공하는 불변 클래스에서는 불필요한 객체 생성을 피할 수 있다.

ex) Boolean(String) 생성자 대신 Boolean.valueOf(String) 팩터리 메서드를 사용하는 것이 좋다.

 

·  가변 객체라 해도 수용 중에 변경되지 않을 것임을 안다면 재사용할 수 있다.

 

·  생성 비용이 아주 비싼 객체가 반복해서 필요하다면, 캐싱하여 재사용하길 권한다.

아이템 7. 다 쓴 객체 참조를 해제하라

 아래 스택 클래스는 메모리 누수가 발생한다.

- 이 스택을 사용하는 프로그램을 오래 실행하다 보면 점차 가비지 컬렉션 활동과 메모리 사용량이 늘어나 결국 성능이 저하된다.

- 드물긴 하지만 심할 때는 디스크 페이징이나 OutOfMemoryError를 일으켜 프로그램이 예기치 않게 종료된다.

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    
    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }
    
    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }
    
    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        return elements[--size];
    }
    
    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

 

· pop 메서드에서 메모리 누수가 발생한다.

- 스택이 커졌다가 줄어들 때, 스택에서 꺼내진 객체들은 프로그램에서 더 이상 사용하지 않더라도 가비지 컬렉터가 회수하지 않는다.

- 꺼내진 객체들이 다 쓴 참조(obsolete reference)를 여전히 가지고 있기 때문이다.

- elements 배열의 '활성 영역'밖의 참조들이 모두 여기에 해당한다. 활성 영역은 인덱스가 size보다 작은 원소들로 구성된다.

- 다쓴 참조: 앞으로 다시 쓰지 않을 참조

 

·  객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐 아니라 그 객체가 참조하는 모든 객체(또 그 객체들이 참조하는 모든 객체)를 회수해가지 못한다.

- 단 몇 개의 객체가 매우 많은 객체를 회수되지 못하게 할 수 있고, 잠재적으로 성능에 악영향을 줄 수 있다.

 

해결 방법

· 참조를 다 썼을 때 null(참조 해제)한다.

public Object pop() {
    if (size == 0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; // 다 쓴 참조 해제
    return result;
}

· 다쓴 참조를 null 하면 프로그램 오류를 조기에 발견할 수도 있다.

- null 처리한 참조를 실수로 사용하려 하면 프로그램은 즉시 NullPointerException을 던지며 종료된다.

- null 처리하지 않았다면, 아무 내색 없이 무언가 잘못된 일을 수행할 수 있다.

 

· 객체 참조를 null 처리하는 일은 예외적인 경우여야 한다. 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내는 것이다.

- 변수의 범위를 최소로 되게 정의했다면(아이템 57) 이 일은 자연스럽게 이뤄진다.

- null 처리를 해야하는 경우는 앞선 코드의 스택 처럼 자기 메모리를 직접 관리하는 클래스이다. 비활성 영역에서 참조하는 객체가 더 이상 쓸모없다는 것을 프로그래머만 알지 가비지 컬렉터는 알지 못하기 때문이다. 이럴 때는 null 처리를 하여 가비지 컬렉터에 직접 알려야한다.

 

· 캐시 역시 메모리 누수를 일으키는 주범이다.

- 객체 참조를 다 쓴 뒤 함참을 그냥 놔두는 일을 자주 접할 수 있다.

- 해결 방법1: 외부에서 키를 참조하는 동안만 엔트리가 살아 있는 캐시가 필요하다면, WeakHashMap을 사용해 캐시를 만든다.

다 쓴 엔트리는 즉시 자동으로 제거된다.

- 해결 방법2: 캐시를 만들 때 보통 캐시 엔트리의 유효 기간을 정확히 정의하기 어렵기 때문에 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식이 흔히 사용된다. 이런 방식에서는 쓰지 않는 엔트리를 이따금 청소해야한다. ScheduledThreadPoolExecutor 같은 백그라운드 스레드를 활용하거나 캐시에 새 엔트리를 추가할 때 부수 작업으로 수행할 수 있다.

- LinkedHahshMap은 removeEldestEntry 메서드를 써서 후자의 방식으로 처리한다.

- 더 복잡한 캐시를 만들고 싶다면 java.lang.ref 패키지를 직접 활용해야 한다.

아이템 8. finalizer와 cleaner 사용을 피하라

· 자바는 finalizer, cleaner 두 가지 객체 소멸자를 제공한다.

· finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요하다.

- 오동작, 낮은 성능, 이식성 문제의 원인이 되기도 한다.

· 자바 9에서 finalizer가 deprecated되고, cleaner를 그 대안으로 소개했지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요하다.

아이템 9. try-finally보다는 try-with-resources를 사용하라

자바 라이브러이에는 close 메서드를 호출해 직접 닫아줘야 하는 자원이 많다.

ex) InputStream, OutputStream, java.sql.Connection

 

· 자원 닫기는 클라이언트가 놓치기 쉬워서 예측할 수 없는 성능 문제로 이어지기도 한다.

- 이런 자원 중 상당수는 안전망으로 finalizer를 활용하지만, finalizer는 믿음직하지 못하다.

 

·  전통적으로 자원을 닫는 수단으로 try-finally가 쓰였지만, 자원이 둘 이상이면 코드가 너무 지저분해진다. 

// 자원 하나 회수
static String firstLineOfFile(String path) throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(path));
    try {
        return br.readLine();
    } finally {
        br.close();
    }
}

 

// 자원 복수개 회수
static void copy(String src, String dst) throws IOException {
    InputStream in = new FileInputStream(src);
    try {
        OutputStream out = new FileOutputStream(dst);
        try {
            byte[] buf = new byte[BUFFER_SIZE];
            int n;
            while ((n = in.read(buf)) >= 0) {
                out.write(buf, 0, n);
            }
        } finally {
            out.close();
        }
    } finally {
        in.close();
    }
}

 

- 심지어 앞선 코드 또한 미묘한 결점이 있다.

· 자바7에서 등장한 try-with-resources은 try-finally의 결점을 해결한다.

- 이 구조를 사용하려면 하당 자원이 AutoCloseable 인터페이스를 구현해야 한다.

- 단순히 void를 반환하는 close 메서드 하나만 정의한 인터페이스다.

 

· 다음은 앞선 코드를 try-with-resources를 사용해 재작성한 코드다.

static String firstLineOfFile(String path) throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader(path))) {
        return br.readLine();
    }
}

 

static void copy(String src, String dst) throws IOException {
    try (InputStream in = new FileInputStream(src);
        OutputStream out = new FileOutputStream(dst)) {
        byte[] buf = new byte[BUFFER_SIZE];
        int n;
        while ((n = in.read(buf)) >= 0) {
            out.write(buf, 0, n);
        }
    }
}

· 앞선 코드에서 확인할 수 있듯이 try-with-resources 버전이 짧고 읽기 수월하며, 문제를 진단하기도 훨씬 좋다.

- firstLineOfFile 메서드를 살펴보자. readLine과 (코드에는 나타나지 않는)close 호출 양쪽에서 예외가 발생하면, close에서 발생한 예외는 숨겨지고 readLine에서 발생한 예외가 기록된다.

- 이렇게 숨겨진 예외들도 스택 추적 내역에 '숨겨졌다(suppressed)'는 꼬리표를 달고 출력된다.

- 자바 7에서 Throwable에 추가된 getSuppressed 메서드를 이용하면 프로그램 코드에서 가져올 수도 있다.

 

· try-with-resources도 catch 절을 쓸 수 있다.

- 이를 통해 try 문을 더 중첩하지 않고 다수의 예외를 처리할 수 있다.

static String firstLineOfFile(String path, String defaultVal) {
    try (BufferedReader br = new BufferedReader(new FileReader(path))) {
        return br.readLine();
    } catch (IOException e) {
        return defaultVal;
    }
}

 

 

01. 영화 예매 시스템

● 요구사항 살펴보기

영화 예매 시스템을 만들 때, 가격에 대한 할인액을 결정하는 두 가지 규칙이 존재할 수 있다.

1. 할인 조건 : 가격의 할인 여부 결정

2. 할인 정책 : 할인 요금을 결정

02. 객체지향 프로그래밍을 향해

●  협력, 객체, 클래스

객체지향 언어에 익숙한 사람이라면 어떤 클래스(class)가 필요한지 고민할 것이다.

-> 이것은 객체지향의 본질과는 거리가 멀다. 말 그대로 객체를 지향하는 것이다.

진정한 객체지향 패러다임으로의 전환? 클래스가 아닌 객체에 초점을 맞추며 두가지에 집중

1. 어떤 클래스보다는 어떤 객체들이 필요한지 고민한다.

- 어떤 객체들이 어떤 상태와 행동을 가지는지 결정

2. 객체를 기능을 구현하기 위해 협력하는 공동체의 일원으로 생각한다.

- 객체는 다른 객체에게 도움을 주거나 의존한다.

- 협력함으로써 설계를 유연하고 확장가능하게 한다.

- 공통된 특성과 상태를 가진 객체를 타입으로 분류하고 이 타입을 기반으로 클래스를 구현해라.

 

● 도메인의 구조를 따르는 프로그램 구조

도메인(domain)이란? 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야

객체지향 패러다임이 강력한 이유는 객체라는 동일한 추상화 기법을 사용할 수 있기 때문이다.

요구사항과 프로그램 객체라는 동일한 관점에서 보기 때문에 도메인을 구성하는 개념들이 프로그램의 객체와 클래스로 연결된다.

 

영화 예매 도메인을 구성하는 타입들의 구조 참조 (책 p.41)

일반적으로 클래스 기반의 객체지향 언어에서는 도메인 개념을 구현하기 위해 Class를 사용한다.

일반적으로 클래스의 이름은 대응되는 도메인 개념의 이름과 동일하거나 유사하게 지어야 한다.

클래스 사이의 관계또 최대한 도메인 개념 사이에 맺어진 관계와 유사하게 만들어야 프로그램의 구조를 이해하고, 예상하기 쉬워진다.

 

그래서 클래스의 구조는 도메인의 구조와 유사한 형태를 띠어야 한다.

도메인 개념의 구조를 따르는 클래스 구조 참조 (책 p.42)

 

● 클래스 구현하기

인스턴스 변수의 가시성은 private이고 메서드의 가시성은 public이다.

클래스는 내부와 외부로 구분되며 경계를 명확하게 해서 객체의 자율성을 보장해야 된다.

외부에서는 객체의 속성에 직접 접근할 수 없도록 막고, 적절한 public메서드를 통해서만 내부 상태를 변경할 수 있게 해야 한다.

 

자율적인 객체

1. 객체가 상태(state)와 행동(behavior)을 함께 가지는 복합적인 존재라는 것

2. 객체가 스스로 판단하고 행동하는 자율적인 존재라는 것

 

예전 : 객체를 상태와 행동을 함께 포함하는 식별 가능한 덩의로 정의

but, 객체지향은 객체라는 단위 안에 데이터와 기능을 한 덩어리로 묶음으로써 문제 영역의 아이디어를 적절하게 표현해줄 수 있게 했다.  (캡슐화)

 

1) 객체지향 프로그래밍 언어들은 상태와 행동을 캡슐화하는 것 외에도 접근 제어(access control) 메커니즘도 제공

2) 많은 프로그래밍 언어들은 접근 제어를 위해 public, protected, private과 같은 접근 수정자(access modifier)를 제공

---> 객체 내부에 대한 접근을 통제하는 이유 : 객체를 자율적인 존재로 만들기 위해, 외부의 간섭 최소화

 

캡슐화와 접근 제어는 객체를 두 부분으로 나눈다.

1) 외부에서 접근 가능한 부분 : 퍼블릭 인터페이스 (public interface)

2) 외부에서는 접근 불가능하고 오직 내부에서만 접근 가능한 부분 : 구현 (implementation)

인터페이스와 구현의 분리 (seperation of interface and implementation) 원칙은 객체지향 프로그램을 만들기 위해 따라야 하는 핵심 원칙이다.

 

일반적으로 객체의 상태는 숨기고 행동만 외부에 공개해야 된다.

클래스의 속성은 private로 감추고, 외부에 제공해야 하는 일부 메서드만 public으로 선언한다.

public interface에는 public으로 지정된 메서드만 포함한다.

서브클래스나 내부에서만 접근 가능해야 한다면 protected나 private로 지정해야한다.

 

프로그래머의 자유

클래스 작성자(class creator) : 새로운 데이터 타입을 프로그램에 추가

클라이언트 프로그래머(client programmer) : 클래스 작성자가 추가한 데이터 타입을 사용

 

class creator는 client programmer 에게 필요한 부분만 공개하고, 나머지는 숨겨야 한다.

class creator입장에서는 client programmer가 숨겨놓은 부분에 마음대로 접근할 수 없도록 방지함으로써 클라이언트 프로그래밍에 대한 영향을 걱정하지 않고도 내부 구현을 마음대로 변경할 수 있다. 이를 구현은닉(implementation hiding)이라고 부른다.

 

이를 통해 client programmer는 내부 구현은 무시한채 인터페이스만 알고 있어도 class를 사용할 수 있기 때문이다.

따라서 client programmer가 알아야 할 지식의 양이 줄어든다.

클래스를 개발할때마다 인터페이스와 구현을 깔끔하게 분리하기 위해 노력해야 한다.

 

● 협력하는 객체들의 공동체

 

 

협력에 관한 짧은 이야기

객체는 다른 객체의 인터페이스에 공개된 행동을 수행하도록 요청(request)할 수 있다. 요청을 받은 객체는 자율적인 방법에 따라 요청을 처리한 후 응답(response)한다.

 

객체가 다른 객체와 상호작용 하는 유일한 방법은 메시지를 전송(send a message)하는 것 뿐이다.

메시지를 수신한 객체는 스스로의 결정에 따라 자율적으로 메시지를 처리할 방법을 결정한다.

이처럼 수신된 메시지를 처리하기 위한 자신만의 방법을 메서드(method)라고 부른다.

 

객체지향 패러다임이 유연하고, 확장 가능하며, 재사용 가능한 설계를 낳는다는 명성을 얻게 된 배경에는 메시지와 메서드를 구문한 것이 한몫한다. 메시지와 메서드의 구분에서부터 다형성(polymorphism)의 개념이 출발한다.

public class Screening {
    private Money calculateFee(int audienceCount) {
        return movie.calculateMovieFee(this).times(audienceCount);
    }
}

'Screening이 Movie 에게 calculateMovieFee 메시지를 전송한다' 라고 말하는것이 적절하다.

사실 Screening은 Movie안에 calculateMovieFee 메서드가 존재하고 있는지 조차 모른다.

단지 Movie 가 calculateMovieFee 메시지에 응답할 수 있다고 믿고 메시지를 전송할 뿐이다.

 

메시지를 받은 Movie는 스스로 적절한 메서드를 선택한다.

03. 할인 요금 구하기

 할인 요금 계산을 위한 협력 시작하기

객체지향에서 중요하다고 여겨지는 두가지 개념 : 상속(inheritance), 다형성 

- 이 두개의 기반에는 추상화(abstraction) 원리가 숨어져 있다.

 

 할인 정책과 할인 조건

이번에는 할인 정책과 할일 조건을 구현해 보자.

 

부모 클래스인 DiscountPolicy 안에 중복 코드를 두고, AmountPolicy, PercentDiscountPolicy가 이 클래스를 상속한다.

실제 애플리케이션에서 DiscountPolicy의 인스턴스를 생성할 필요가 없기 때문에 abstract class 로 구현했다.

public abstract class DiscountPolicy {
    private List<DiscountCondition> conditions = new ArrayList<>();

    public DiscountPolicy(DiscountCondition ... conditions) {
        this.conditions = Arrays.asList(conditions);
    }

    Money calculateDiscountAmount(Screening screening){
        for(DiscountCondition each : conditions){
            if(each.isSatisfiedBy(screening)){
                return getDiscountAmount(screening);
            }
        }

        return Money.ZERO;
    }

    protected abstract Money getDiscountAmount(Screening screening);
}

DiscountPolicy는 할인 여부와 요금 계산에 필요한 전체적인 흐름을 정의하지만, 실질적인 요금을 계산하는 부분을 추상메서드 인

getDiscountAmount() 메서드에 위임한다.

 

이처럼 기본적인 알고리즘의 흐름을 결정하고, 중간에 필요한 처리를 자식 class에게 위임하는 디자인 패턴을 TEMPLATE METHOD 패턴 이라 부른다.

 

 할인 정책 구성하기

 

04. 상속과 다형성

public class Movie {
    private String title;
    private Duration runningTime;
    private Money fee;
    private DiscountPolicy discountPolicy;

    public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
        this.title = title;
        this.runningTime = runningTime;
        this.fee = fee;
        this.discountPolicy = discountPolicy;
    }

    public Money getFee() {
        return fee;
    }

    public Money calculateMovieFee(Screening screening) {
        return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }
}

Movie 클래스 어디를 봐도 할인 정책이 금액 할인 정책인지?, 비율 할일 정책인지? 판단하지 않는다.

Movie 내부에 할인 정책을 결정하는 조건문도 없는데도 불구하고 어떻게 영화 요금을 계산할때 할인 정책을 선택할수 있을까?

 

이는 상속과 다형성을 통해 가능하다.

 

 컴파일 시간 의존성과 실행 시간 의존성

DiscountPolicy 상속계층 그림 참조 (책 p. 57)

위 클래스 다이어 그램을 보면, Movie 클래스가 DiscountPolicy와 연관관계를 맺고 있다.

문제는 영화 요금을 계산하기 위해서는 abstarc class가 아니라, AmountDiscountPolicy, PercentDiscountPolicy 와 같은 인스턴스에 의존해야 한다.

하지만 Movie 클래스는 두 클래스 중 어떤것도 의존하지 않는다. DiscountPolicy 추상 클래스에만 의존하고 있다.

 

그럼 Movie 코드를 작성하던 시점에는 존재조차 모르던 AmountDiscountPolicy, PercentDiscountPolicy의 인스턴스를 사용할수 있는 이유는 무엇일까?

 

다음은 아바타 영화에 대한 코드이다.

new Movie("아바타", Duration.ofMinutes(120), Money.wons(10000),
        new AmountDiscountPolicy(Money.wons(800), ...));

Movie의 인스턴스를 생성할때 생성자의 인자로 AmountDiscountPolicy 를 전달하면 된다.

 

여기서의 핵심은 코드의 의존성과 실행시점의 의존성이 서로 다를 수 있다는 점이 중요하다.

클래스 사이의 의존성과 객체 사이의 의존성은 동일하지 않을 수 있다. 

 

설계가 유연해질수록 코드를 이해하고 디버깅하기는 점점 더 어려워진다.

반면 유연성을 억제하면 코드를 이해하고 디버깅 하기는 쉬워지지만, 재사용성과 확장 가능성은 낮아진다.

 

 차이에 의한 프로그래밍

의미 : 부모 클래스와 다른 부분만을 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법

클래스를 하나 추가하고 싶은데 그 클래스가 기존의 어떤 클래스와 매우 흡사할 때, 약간만 추가하거나 수정하면 좋을 것이다. 이걸 가능하게 해주는게 상속이다.

상속 : 기존 클래스가 가지고 있는 모든 속성과 행동을 새로운 클래스에 포함시킬 수 있다.

 

 상속과 인터페이스

인터페이스는 객체가 이해할 수 있는 메시지의 목록을 정의한다는 것을 기억하자.

상속을 통해 자식 클래스는 자신의 인터페이스에 부모 클래스의 인터페이스를 포함하게 된다.

상속을 통해 자식은 부모 클래스가 수신할 수 있는 모든 메시지를 수신할수 있기 때문에 외부 객체는 자식 클래스를 부모 클래스와 동일한 타입으로 간주할 수 있다.

정리하자면 자식 클래스는 상속을 통해 부모 클래스의 인터페이스를 물려받기 때문에 부모 클래스 대신 사용될 수 있다.

 

자식 클래스가 부모 클래스를 대신하는 거을 업캐스팅(upcasting)이라고 부른다. (책 p.62 그림 참조해도 좋을듯)

 

 다형성

메시지와 메서드는 다른 개념이다.

Movie는 discountPolicy의 인스턴스 에게 calculateDiscountPolicy 메시지를 전송한다.

그럼 실행되는 메서드는 무엇인가? 이는 연결된 객체의 클래스가 무엇인지에 따라 달라진다.

 

다시말해, Movie는 동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것 인가? 는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라진다. 이를 다형성 이라 부른다.

(즉, 동일한 메시지에 대하여 객체의 타입에 따라 다르게 응답할수 있는 능력을 의미한다.)

 

다형성 : 객체지향 프로그램의 컴파일 시간 의존성과 실행 시간 의존성이 다를 수 있다는 사실을 기반으로 한다.

동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력을 의미

 

이처럼 메시지와 메서드를 실행 시점에 바인딩 하는것을 지연 바인딩(lazy binding) 또는 동적 바인딩(dynamic binding)이라 부른다.

반면에 전통적인 컴파일 시점에 실행될 함수나, 프로시저를 결정하는 것을 초기 바인딩(early binding), 정적 바인딩(static binding)이라 부른다.

 

 인터페이스와 다형성

자바에서는 인터페이스라는 프로그래밍 요소를 제공한다.

구현에 대한 고려 없이 다형적인 협력에 참여하는 클래스들이 공유 가능한 외부 인터페이스를 정의한 것

05. 추상화와 유연성

 추상화의 힘

지금까지 살펴본 코드들에서 DiscountPolicy는 AmountDiscountPolicy, PercentDiscountPolicy 보다 더 추상적이고,

DiscountCondition 또한 더 추상적이다.

 

이는 인터페이스에 초점을 맞추기 때문이다.

DiscountPolicy는 모든 할인 정책이 수신할 수 있는 calculateDiscountAmount 메시지를 정의한다.

DiscountCondition은 모든 할인 조건들이 수신할 수 있는 isSatisfiedBy 메시지를 정의한다.

그림, 추상화는 좀 더 일반적인 개념들을 표현한다 (책 p.65 참조)

추상화를 사용할 경우 2가지 장점을 보여준다.

1) 추상화의 계층만 따로 떼어 놓고 살펴보면 요구사항의 정책을 높은 수준에서 서술할 수 있다.

2) 추상화를 아용하면 설계가 좀 더 유연해진다.

 

위 다이어 그램을 하나의 문장으로 정리하면 "영화 예매 요금은 최대 하나의 할인정책 과 다수의 할인조건을 이용해 계산할 수 있다"와 같다.

위 문장은 "금액할인 정책과 두개의 순서조건, 한개의 기간 조건을 이용해 계산할 수 있다" 라는 문장을 포괄할수 있다는 사실이 중요하다.

 

이는 좀더 추상적인 개념들을 사용해 문장을 작성했기 때문이다.

 

추상화를 통해 상위 정책을 기술한다는 것은 기본적인 어플리케이션의 협력 흐름을 기술한다는것을 의미한다.

영화의 예매 가격을 계산하기 위한 흐름은 항상 Movie -> DIscountPolicy -> DiscountCondition 로 흘러간다.

할인 정책이나, 조건의 자식들은 추상화를 이용해서 정의한 흐름에 그대로 따르게 된다.

 

 유연한 설계

 

 추상클래스와 인터페이스 트레이드오프

public abstract class DiscountPolicy {
    private List<DiscountCondition> conditions = new ArrayList<>();

    public DiscountPolicy(DiscountCondition ... conditions) {
        this.conditions = Arrays.asList(conditions);
    }

    public Money calculateDiscountAmount(Screening screening) {
        for(DiscountCondition each : conditions) {
            if (each.isSatisfiedBy(screening)) {
                return getDiscountAmount(screening);
            }
        }

        return Money.ZERO;
    }

    abstract protected Money getDiscountAmount(Screening Screening);
}
public class NoneDiscountPolicy entends DiscountPolicy {

    @Override
    protected Money getDiscountAmount(Screening screening) {
    	return Money.ZERO;
    }
}

위 코드를 보면, NoneDiscountPolicy는 getDiscountAmount() 를 오버라이딩 하여 ZERO를 반환하도록 하고있다.

하지만 할인조건이 없는경우, calculateDiscountAmount 메시지 호출시 getDiscountAmount() 자체를 호출하지 않는다.

 

부모 클래스인 DiscountPolicy는 할인 조건이 없는경우 getDiscountAmount()를 호출하지 않고, ZERO를 반환한다.

이는 DiscountPolicy내부에 NoneDiscountPolicy를 개념적으로 결합시켜둔 것 이다.

개발자는 DiscountCondition이 없으면 0원을 반환할 것이라는 사실을 가정하기 때문이다.

 

이러한 문제를 해결하기 위해 DiscountPolicy를 인터페이스로 바꾸고, 기존의 DiscountPolicy를 DefaultDiscountPolicy 라는 abstract class로 만든다.

 

과연 어떤 설계가 더 좋은가?

구현과 관련된 모든 것들이 트레이드 오프의 대상이 될 수 있다.

우리가 작성하는 코드는 모두 합당한 이유가 있어야 한다. 비록 아주 사소하더라도 트레이트오프를 통해 얻어진 결론은 그렇지 않은 경우와 매우 다르다.

 

코드 재사용

객체지향을 조금이라도 공부해봤다면 코드 재사용을 위해서는 상속이 아닌, 합성을 사용해야 함을 배웠을 것이다.

Movie가 DiscountPolicy의 코드를 재사용 하는 방식이 바로 합성이다.

합성은 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법을 말한다.

 

다음은 이 합성 방식을 상속으로 변경한 다이어그램이다.

그림, 책 p.70 참조

 

많은 사람들이 상속 대신 합성을 선호하는 이유가 뭘까?

 

 상속

상속은 2가지 이유에서 설계에 좋지 못하다

1) 캡슐화를 위반한다 : 부모클래스의 구현이 자식클래스에 노출되기 때문에 캡슐화가 약화된다.

2) 설계를 유연하지 못하게 만든다 : 상속은 부모클래스와 자식클래스 사이의 관계를 컴파일 시점에 결정한다. 따라서 실행 시점에 객체의 종류를 변경하는 것은 불가능 하다.

 

 합성

합성 : 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법

1) 인터페이스에 정의된 메시지를 통해서만 재사용 가능하기 때문에 구현을 효과적으로 캡슐화할 수 있다.

2) 의존하는 인스턴스를 교체하는 것이 비교적 쉽기 때문에 설계를 유연하게 만든다.

상속은 클래스를 통해 강하게 결합되는 데 비해 합성은 메시지를 통해 느슨하게 결합된다.

 

문제

https://www.acmicpc.net/problem/11660

 

공부한 부분

2차원 배열일 때 구간합 구하는 법에 대해 공부했다.

1. 2차원 구간 합 배열 D[X][Y] 정의

D[X][Y] = 원본 배열의 (0,0)부터 (X,Y)까지의 사각형 영역 안에 있는 수의 합

2. D[i][j]의 값을 채우는 구간 합 공식

D[i][j] = D[i-1][j] + D[i][j-1] - D[i-1][j-1] + A[i][j]

3. 질의 X1,Y1,X2,Y2에 대한 답을 구간합으로 구하는 방법

D[X2][Y2] - D[X1-1][Y2] - D[X2][Y1-1] + D[X1-1][Y1-1]

 

Java Code

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int N = Integer.parseInt(st.nextToken());
        int M = Integer.parseInt(st.nextToken());
        int[][] map = new int[N + 1][N + 1];
        for (int i = 1; i <= N; i++) {
            st = new StringTokenizer(br.readLine());
            for (int j = 1; j <= N; j++) {
                map[i][j] = Integer.parseInt(st.nextToken());
            }
        }
        int[][] sum = new int[N + 1][N + 1];
        for (int i = 1; i <= N; i++) {
            for (int j = 1; j <= N; j++) {
                sum[i][j] = sum[i][j - 1] + sum[i - 1][j] - sum[i - 1][j - 1] + map[i][j];
            }
        }
        for (int i = 0; i < M; i++) {
            st = new StringTokenizer(br.readLine());
            int X1 = Integer.parseInt(st.nextToken());
            int Y1 = Integer.parseInt(st.nextToken());
            int X2 = Integer.parseInt(st.nextToken());
            int Y2 = Integer.parseInt(st.nextToken());
      
            int answer = sum[X2][Y2] - sum[X1 - 1][Y2] - sum[X2][Y1 - 1] + sum[X1 - 1][Y1 - 1];
            System.out.println(answer);
        }
    }
}

'Algorithm > BOJ' 카테고리의 다른 글

[백준 2164] 카드2 (Java)  (0) 2023.02.07
[백준 1316] 그룹 단어 체커(Java)  (0) 2023.01.26
백준 11659. 구간 합 구하기 4 (Java)  (0) 2022.08.12
백준 1546번. 평균 (Java)  (0) 2022.08.12
백준 11720. 숫자의 합 (Java)  (0) 2022.08.12

문제

https://www.acmicpc.net/problem/11659

 

공부한 부분

구간합 구하는 법에 대해 공부했다.

1. 합 배열을 구한다.

S[i] = S[i-1]+A[i]

2. 구간 합을 구한다.

구간합 = S[j] - S[i]

Java Code

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class boj_003_11659 {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int N = Integer.parseInt(st.nextToken());
        int M = Integer.parseInt(st.nextToken());
        int[] num = new int[N+1];
        int sum = 0;
        int[] sumArr = new int[N+1];
        st = new StringTokenizer(br.readLine());
        for (int i = 1; i <= N; i++) {
            num[i] = Integer.parseInt(st.nextToken());
            sum += num[i];
            sumArr[i] = sum;
        }
        for (int i = 1; i <= M; i++) {
            st = new StringTokenizer(br.readLine());
            int A = Integer.parseInt(st.nextToken());
            int B = Integer.parseInt(st.nextToken());
            System.out.println(sumArr[B]-sumArr[A-1]);

        }
    }
}

문제

https://www.acmicpc.net/problem/1546

 

풀이

방법1. 

처음에는 입력을 받으면서 최대값을 구하고,

입력된 모든 값을 점수/M*100으로 고치면서 총합을 구해서 평균을 구했는데 이렇게 되면 for문을 2번 돌게 된다.

방법2.

어차피 (A/M*100+B/M*100+C/M*100)/3 = (A+B+C)/M*100 이니까 for문 한번에 해줄 수 있었다. (2번째 방법)

 

Java Code (방법1)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class boj_002_1546 {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int N = Integer.parseInt(st.nextToken());
        double[] score = new double[N];
        double max = Double.MIN_VALUE;
        st = new StringTokenizer(br.readLine());
        for (int i = 0; i < score.length; i++) {
            score[i] = Double.valueOf(st.nextToken());
            max = Math.max(score[i],max);
        }
        double sum = 0.0;
        double[] changedScore = new double[N];
        for (int i = 0; i < score.length; i++) {
            changedScore[i] = score[i]/max*100;
            sum +=changedScore[i];
        }

        System.out.println(sum/N);
    }
}

Java Code (방법2)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class boj_002_1546_2 {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int N = Integer.parseInt(st.nextToken());
        double[] score = new double[N];
        double max = Double.MIN_VALUE;
        double sum = 0.0;
        st = new StringTokenizer(br.readLine());
        for (int i = 0; i < score.length; i++) {
            score[i] = Double.valueOf(st.nextToken());
            max = Math.max(score[i],max);
            sum += score[i];
        }
        double answer = sum/max*100/N;


        System.out.println(answer);
    }
}

 

문제

https://www.acmicpc.net/problem/11720

N개의 숫자가 공백 없이 쓰여있을 때, 숫자의 합을 출력하는 문제

 

공부한 부분

1. 공백 없이 숫자가 주어질 때 입력 값 받는 방법

12345 이런식으로 입력이 주어지고, 1,2,3,4,5 숫자를 따로 사용해야 할 때

일단 String 값으로 12345를 받고, char형 배열에 toCharArray를 사용해서 분리해준 후

해당 char형 값에 -'0' 해주어 int 값으로 바꿔준다.

 

숫자 CHAR(0~9)는 ASCII 코드 48부터 시작하므로, 48을 빼주면 숫자를 얻을 수 있다.

char c = '5';
int n = 0;
n = c - 48;

 

char c = '5';
int n = 0;
n = c - '0';

 

Code

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class boj_001_11720 {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        int N = Integer.parseInt(br.readLine());
        String sNum = br.readLine();
        char[] cNum = sNum.toCharArray();

        int sum = 0;
        for (int i = 0; i < N; i++) {
            sum += cNum[i]-'0';
        }
        System.out.println(sum);
    }
}

'Algorithm > BOJ' 카테고리의 다른 글

백준 11659. 구간 합 구하기 4 (Java)  (0) 2022.08.12
백준 1546번. 평균 (Java)  (0) 2022.08.12
백준 15927. 회문은 회문아니야!!(Java)  (0) 2022.08.11
[백준 2493] 탑 (Java)  (0) 2022.01.05
[백준 10211] Maximum Subarray (Java)  (0) 2022.01.05

+ Recent posts