문제 설명

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr


풀이 방법

접근 방식

    ◦  라이언이 어피치를 가장 큰 점수로 차이로 어피치를 이길 수 있는 경우를 구해야 하는 문제이다.

          -  점수차이가 나는데 특별한 수학적 규칙이 없고, 라이언이 화살을 어떻게 쏘는가 그 결과에 달려있다.

          -  화살을 쏠 수 있는 모든 경우의 수를 구하고, 그 결과를 비교해 점수차의 최댓값을 구하는 문제이다.

    ◦  보통 여러가지 경우의 수에 대한 결과를 비교할 땐 BFS를 사용하는게 편하다. 이 문제도 마찬가지!

 

핵심 아이디어

    ◦  10점 ~ 0점 과녁을 순회하면서, 각 점수 과녁에서 라이언이 할 수 있는 선택은 2가지이다.

          -  화살을 쏘지 않고 아껴둔다. (사용한 화살 : 0개)

          -  어피치가 쏜 화살보다 1개 더 쏜다. (사용한 화살 : 어피치가 쏜 화살 +1개)

    ◦  과녁은 10점 ~ 0점 총 11개 이고, 선택은 2가지 이니까 총 2의 11제곱에 대한 경우의 수를 자식 노드가 2개인 이진트리로 집계한 후
         결과를 비교하면 된다.

 

알고리즘

    ◦  이진트리 BFS

          -  각 점수 과녁을 순회하면서 화살을 쏘거나, 쏘지 않거나로 자식 노드를 확장해 나간다.

          -  노드 확장 종료 조건 = 화살을 다 쐈음 or (10점~0점 과녁 순으로 순회할 때)현재 과녁이 마지막 과녁임

              -  현재 과녁이 마지막 과녁인 경우, 남은 화살을 전부 쏜다.

              -  또한 어피치보다 무조건 1개 더 쏘는 상황이 반복되다보면 라이언이 쏠 수 있는 화살 개수보다 더 쏘게되는 상황이
                  발생할 수 있다. 이는 경기상에서 고려하지 않는 상황이므로, 이 케이스는 무시하는 로직을 추가해 줘야 한다.

          -  노드 확장이 종료되면 그 케이스의 라이언과 어피치의 점수 차이를 구한다.

          -  모든 경우의 수를 돌면서, 점수 차이가 최대가 되는 값을 계속 업데이트한다.


파이썬 코드

def solution(n, info):
    # set variables
    bfs = [(0, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])]
    maxScore = 0
    answer = []
    
    while(bfs):
        # 현재 쏘려는 점수, 과녁 현황
        idx, arrowList = bfs.pop(0)
        
        # 종료 조건1 : 화살을 모두 쏜 경우
        if(sum(arrowList) == n):
            apeach, lion = 0, 0
            # 점수 차이 집계
            for i in range(11):
                if(arrowList[i]+info[i] == 0):
                    continue
                if(arrowList[i] <= info[i]):
                    apeach += 10 - i
                else:
                    lion += 10 - i
            # 라이언이 이긴 경우 : 점수 차이 최댓값 업데이트
            if apeach < lion:
                if maxScore > (lion - apeach):
                    continue
                if maxScore <= (lion - apeach):
                    maxScore = lion - apeach
                    answer = arrowList.copy()
            
        # 종료 조건2 : 화살을 오버해서 쏜 경우 (무시)
        elif(sum(arrowList) > n):
            continue
        
        # 종료 조건3 :  마지막 과녁인데 화살이 남은 경우 (남은 화살을 다 쏜다)
        elif(idx == 10):
            arrowList[10] = n - sum(arrowList)
            bfs.append((-1,arrowList))
        
        # 경기 진행중
        else:
            # 어피치보다 1발 더 쏘기
            temp = arrowList.copy()
            temp[idx] = info[idx]+1 
            bfs.append((idx+1, temp))
            
            # 쏘지 않기
            temp = arrowList.copy()
            temp[idx] = 0 
            bfs.append((idx+1, temp))
    
    # return answer
    if(not answer):
        return [-1]
    return answer

메모리 관리가 필요한 이유

• 멀티프로그래밍 환경으로 변화하면서 한정된 메모리를 효율적으로 사용할 필요가 생겼다.

• 프로세스는 독립된 메모리 공간을 갖고, 운영체제 혹은 다른 프로세스의 메모리 공간에 접근할 수 없기 때문에

    이를 자체적으로 관리하지 못한다.

• 운영체제만 메모리 영역과 프로세스 메모리 공간의 접근에 제약을 받지 않으므로 운영체제에서 이를 관리해줄 필요가 있다.

 

• 프로세스 주소

    ◦  논리적 주소(Logical address)

          -  CPU가 생성하는 주소. 가상 주소(Virtual address)라고도 부른다.

          -  프로세스마다 독립적으로 가지는 주소 공간 (힙/데이터/텍스트/스택 공간 등)

          -  프로세스 내부에서 사용하며 각 프로세스 마다 0의 주소 부터 시작한다.

    ◦  물리적 주소(Physical address)

          -  프로세스가 실행되기 위해 실제로 메모리(RAM)에 올라가는 위치

          -  Address Binding : 프로세스가 메모리의 어느 위치(어떤 물리적 주소)에 올라갈지 결정하는 작업

 

• 연속 할당 기법

    ◦  프로그램 전체를 하나의 커다란 공간에 연속적으로 할당하는 기법

    ◦  물리적 메모리를 다수의 분할로 나누어 하나의 분할에 하나의 프로세스가 적재되도록 한다.

    ◦  크게 고정 분할 방식가변 분할 방식으로 나뉜다.

 

• 불연속 할당 기법

    ◦  하나의 프로세스가 사용하는 메모리 공간이 연속적이어야 한다는 제약을 없앤 메모리 관리 방법

    ◦  프로그램의 일부가 서로 다른 주소 공간에 할당될 수 있다.

    ◦  불연속 할당 기법에서 등장하는 개념들

          -  페이지 : 고정 사이즈의 작은 프로세스 조각

          -  프레임 : 페이지 크기와 같은 주기억장치 메모리 조각

          -  단편화 : 기억 장치의 빈 공간 or 자료가 여러 조각으로 나뉘는 현상

          -  세그먼트 : 서로 다른 크기를 가진 논리적 블록이 연속적 공간에 배치되는 것


고정 분할 방식

• 물리적 메모리를 정해진 개수의 영구적인 분할로 나누어두고 각 분할에 하나의 프로세스를 적재하는 방식

• 동시에 메모리에 올릴 수 있는 프로그램 수가 고정되어 있으며, 수행 가능한 프로그램의 최대 크기 또한 제한되기 때문에

    가변분할 방식에 비해 융통성이 떨어진다.

• Internal/External Fragmentation이 발생할 수 있다.


가변 분할 방식

• 메모리에 적재되는 프로그램의 크기에 따라 분할의 크기, 개수를 동적으로 조절하는 방식

• 프로그램의 크기를 고려하여 메모리를 할당하고, 이를 관리할 수 있는 기술이 필요한 방식이다.

• Internal Fragmentation은 발생하지 않지만, 메모리에 새로운 프로세스를 올릴 공간이 충분하지 않으면

    External Fragmentation이 발생할 수 있다.

    ◦  이는 컴팩션(compaction) 기법으로 해결 가능하지만, 비용이 매우 많이 든다.

          -  컴팩션 기법이란? : 중간중간 사용하지 않은채 뻥 뚫려있는 메모리 영역을 한쪽으로 모으기 위해 자원들을 한쪽으로 몰아넣는 기법

          -  수행중인 프로세스의 물리적 메모리 위치를 하나하나 옮겨야 하므로 비용이 매우 크다.

 

가변 분할 방식에서는 메모리 공간의 어디에 프로세스를 올려야 할지 결정해야 한다. (동적 메모리 할당 기법)

이를 결정하는 방법으로 아래 세가지 정도가 있다.

    ◦  최초 적합 방법

          -  가용 공간을 차례대로 살펴보면서 프로그램 크기 < 가용 공간 크기가 최초로 발견될 때 할당

          -  모든 가용 공간을 탐색할 필요가 없어 시간적으로 효율적이다.

    ◦  최적 적합 방법

          -  프로세스 크기 < 가용 공간 크기인 가장 작은 공간을 찾아 할당

          -  모든 가용 공간 리스트가 크기 순으로 정렬되어 있지 않으면 탐색에 오버헤드가 발생할 수 있다.

    ◦  최악 적합 방법

          -  프로세스 크기 < 가용 공간 크기인 가장 작은 큰 공간을 찾아 할당

          -  최적 적합과 동일하게 탐색 오버헤드가 발생할 수 있다.


참고자료

 

[OS] 메모리 연속할당 - 고정분할 방식과 가변분할 방식

[OS] 메모리 연속할당 - 고정분할 방식과 가변분할 방식 안녕하세요? 장장스입니다. 실제 물리적 메모리는 크게 연속할당 방식과 불연속할당 방식으로 나뉩니다. 오늘은 메모리 연속할당 방

zangzangs.tistory.com

 

[메모리관리] 연속할당 방식 - 고정 분할 방식과 가변 분할 방식

연속 할당 방식 프로그램을 메모리에 올릴 때, 물리적 메모리의 한 곳에 연속적으로 적재하는 방식으로 고정 분할 방식과 가변 분할 방식이 존재합니다. 고정 분할 방식 주어진 개수 만큼의 영

junyng.tistory.com

교착 상태(Deadlock) 개념

• 교착상태란? 시스템 자원에 대한 요구가 뒤엉킨 상태

• 둘 이상의 프로세스가 다른 프로세스가 점유하고 있는 자원을 서로 기다릴 때 무한정 대기 상태에 빠지는 상황

• 데드락 발생 조건 (하나라도 성립하지 않으면 이는 해결 가능한 상황)

    ◦  상호 배제(Mutual exclusion)

          -  자원은 한번에 한 프로세스만 사용할 수 있음

    ◦  점유 대기(Hold and wait)

          -  최소한 하나의 자원을 점유하고 있으면서, 다른 프로세스에 할당되어 있는 자원을 추가로 점유하기 위해 대기하고 있는 상태

    ◦  비선점(No preemption)

          -  다른 프로세스에 할당된 자원은 사용이 끝날 때까지 강제로 뺏을 수 없음

    ◦  순환 대기(Circular wait)

          -  각 프로세스가 순환하는 형태로 다음 프로세스가 요구하는 자원을 가지고 있음


교착 상태를 해결하는 방법

예방(prevention)

데드락 발생 조건 중 하나를 제거함으로써 해결하는 방법

    ◦  상호배제 부정 : 여러 프로세스가 공유 자원을 사용할 수 있도록 허용

    ◦  점유대기 부정 : 프로세스 실행 전 모든 자원을 할당

    ◦  비선점 부정 : 자원을 점유 중인 프로세스가 다른 자원을 요구할 때, 가진 자원은 반납하도록 제한

    ◦  순환대기 부정 : 자원에 고유번호 할당하여 순서대로 자원을 요구하도록 제한

• 단점

    ◦  시스템 처리량 및 효율성을 떨어트린다

 

회피(avoidance)

• 데드락이 발생하지 않도록 알고리즘을 설계하는 방법

• 은행원 알고리즘(Banker's Algorithm)

    ◦  은행이 항상 최소 한 명에게 대출해줄 금액은 보유하고 있어야 함에서 유래

    ◦  해당 고객이 대출금을 반환하면 생긴 돈으로 다른 사람의 대출을 해결할 수 있다.

 

    ◦  특정 프로세스에 자원을 할당해주고 반환받을 수 있는 상태 = 안전 상태

    ◦  프로세스에 자원을 할당해줄수도, 돌려 받을수도 없는 상태 = 불안전 상태

          -  교착상태는 불안전상태에 속한다. (T)

          -  불안전상태는 교착상태에 속한다 (F)

    ◦  안전상태면 자원을 할당하고, 불안전 상태면 다른 프로세스들이 자원을 반환할 때까지 대기함으로써 교착 상태가 발생하지 않는

         환경을 조성하는 방법이다.

 

    ◦  이 알고리즘이 제대로 수행되기 위해서는 아래의 3가지 정보가 필요하다.

          -  각 고객들이 최대 얼마의 돈을 요구할지 (Max) = 각 프로세스가 자원을 최대 얼마나 요청할 수 있는지

          -  고객들이 현재 빌린 돈이 얼마인지 (Allocated) = 각 프로세스가 현재 보유하고 있는 자원이 얼마인지

          -  은행이 보유한 돈이 얼마인지, 빌려줄 수 있는 돈이 얼마인지 (Available) = 시스템이 자원을 얼마나 보유하고 있는지.

• 단점

    ◦  할당할 수 있는 자원의 수가 일정해야 한다. (자원 이용도가 낮다.)

    ◦  사용자 수가 일정해야 한다.

    ◦  최대 자원 요구량을 미리 알아야 한다.

    ◦  프로세스들은 유한한 시간 안에 자원을 반납해야 한다.

    ◦  복잡하고, 자원 이용에 제약이 많고, 알고리즘 수행시 오버헤드가 발생할 수 있다.

    ◦  즉 실제 돌아가는 프로그램에 적용하기 상당히 어렵다. → 현재 주로 채택되는 방식이 아님

 

탐지(Detection), 회복(Recovery)

• 교착 상태의 발생을 허용한 다음 회복시키는 방법

• 탐지(Detection)

    ◦  Allocation, Request, Available 등으로 시스템에 데드락이 발생했는지 여부를 탐색하는 방법

    ◦  은행원 알고리즘에서와 유사하게 현재 시스템의 자원 할당 상태를 가지고 파악할 수 있다.

    ◦  자원 할당 그래프를 통해 교착 상태를 탐지하는 방법도 있다.

          -  단, 이 방법은 자원 할당 그래프를 만드는데 오버헤드가 발생할 수 있으니 주의가 필요하다.

• 회복(Recovery)

    ◦  교착 상태 일으킨 프로세스를 종료하거나, 할당된 자원을 해제시켜 회복하는 방법

    ◦  프로세스 종료 방법은 아래의 두가지 종류가 있다.

          -  교착 상태에 놓인 프로세스 전체를 종료시키는 방법

          -  교착 상태가 제거될 때까지 프로세스를 하나씩 종료시키는 방법

    ◦   할당된 자원을 해제시켜 회복하는 방법 : 자원 선점

          -  교착 상태의 프로세스를 일시정지 시키고, 해당 프로세스가 점유하고 있는 자원을 선점해 다른 프로세스에 할당하는 방식이다.

          -  우선 순위가 낮은 프로세스 / 수행 횟수가 적은 프로세스가 우선적으로 선점된다.

 

무시

• 회복과정의 성능저하가 심하다면 해당 교착 상태를 무시하는게 오히려 좋을 수 있다.

• 무시된 교착상태의 프로세스들은 시간이 지나면 자연스럽게 time-out 되어 종료된다.

• 현대의 운영체제들이 가장 많이 채택하는 방법이라고 한다 (!)


식사하는 철학자 문제

• 교착 상태를 설명하는 대표적인 알고리즘 문제로, 문제의 내용은 아래와 같다.

    ◦   철학자가 생각에 빠져 있을 때는 아무런 액션을 취하지 않지만, 때때로 배가 고파진다. 밥을 먹기 위해서는 포크 두 개가 필요하다.

    ◦   왼쪽 포크가 사용 가능해질 때까지 대기한다. 만약 사용 가능하다면 집어든다.

    ◦   오른쪽 포크가 사용 가능해질 때까지 대기한다. 만약 사용 가능하다면 집어든다.

    ◦   포크 두 개를 잡으면 일정 시간 동안 밥을 먹을 수 있다.

    ◦   식사가 끝나면 사용한 포크 두 개를 내려놓는다.

    ◦   철학자는 다시 생각에 빠진다.

    ◦   배가 고픈데 일정 시간동안 식사를 못하면 철학자는 굶어죽는다!

 

• 이 때 만약 모든 철학자가 동시에 왼쪽 포크를 잡으면, 모두 오른쪽의 포크가 사용 가능해질 때까지 기다려야 한다.

    ◦   모든 철학자가 영원히 3번 상태에 머물러있는 교착 상태가 발생한다.

    ◦   무한 교착 상태로 인해 기아 현상이 발생하고, 철학자는 굶어 죽게된다!

 

• 몇가지 해결 방법

    ◦   왼쪽 포크를 들고나서 오른쪽 포크를 들 수 있나 보고, 안되면 포크를 내려놓는다.

          그리고 랜덤한 시간동안 기다린 다음 이를 다시 시도한다.

          -  모두가 동시에 왼쪽 포크를 잡아도 해도 서로 기다리는시간이 다르기 때문에 결국 누군가는 식사를 할 수 있지만,

              낮은 확률로 starvation이 발생할 수 있다.

          -  기아 현상이 절대 발생하면 안되는 환경(원자력발전소, 비행기 등)에서는 사용 불가능한 방법이다.

    ◦   식사할 수 있는 상황을 하나의 뮤텍스(Mutex)로 관리한다.

          -  한번에 한 명씩 반드시 식사를 할 수 있다. 하지만 한 명이 밥을 먹는 동안 맞은편의 철학자도 포크 두개를 잡을 수 있는 상황이다.

             즉 교착상태가 발생하지 않음이 보장되기는 하나, 자원의 낭비가 발생한다.

    ◦   세마포어와 몇가지 규칙을 잘 사용하면 식사하는 철학자 문제를 완벽하게 해결할 수 있다.

          -  간단히 설명하면 철학자의 상태를 식사 중, 식사 대기 중으로 표현하고, 자신의 양쪽이 식사 중이 아니면 식사를 할 수 있도록

             세마포어 형태로 관리하는 방법이다. 이 방법을 사용하면 주어진 상황에서 최대한 많은 철학자들이 교착상태 없이 식사를 할 수

             있다.


참고자료

 

데드락 (DeadLock, 교착 상태) | 👨🏻‍💻 Tech Interview

데드락 (DeadLock, 교착 상태) 두 개 이상의 프로세스나 스레드가 서로 자원을 얻지 못해서 다음 처리를 하지 못하는 상태 무한히 다음 자원을 기다리게 되는 상태를 말한다. 시스템적으로 한정된

gyoogle.dev

 

[운영체제] 데드락(Deadlock, 교착 상태)이란?

컴퓨터/IT/알고리즘 정리 블로그

chanhuiseok.github.io

 

식사하는 철학자 문제(Dining Philosopher Problem)

목표 식사하는 철학자 문제를 해결할 수 있다. 식사하는 철학자 문제란? 철학자 다섯 명이 원형 식탁에 앉아 밥을 먹으려고 합니다. 그들의 양쪽엔 각각 젓가락 한 짝씩 놓여 있고, 밥을 먹으려

jjangsungwon.tistory.com

 

예외는 오직 예외 상황에만 사용할 것

예외를 제어 흐름용으로 사용하지 말자

• 예외는 오직 예외 상황에서만 사용해야 한다.

• 일반적인 제어 흐름용으로 사용해서는 안됨

    ◦  코드를 헷갈리게 한다(코드가 직관적이지 못하다).

    ◦  성능 저하의 원인이 될 수 있다.

    ◦  엉뚱한 곳에서 발생한 버그를 숨겨버릴 수 있다.

 

예외를 제어 흐름용으로 사용한 예시

try {
	int i = 0;
	while(true) { mountainList[i++].climb(); }
} catch(ArrayIndexOutOfBoundsException e) {...}
for(Mountain m : mountainList) { m.climb(); }

• 두 코드 모두 mountainList 배열의 원소를 순회하는 목적으로 쓰여졌다.

• 예외를 제어흐름용으로 사용하니 코드가 전혀 직관적이지 못하다.

• 이 작자는 코드를 왜 이렇게 짰을까?

    ◦  JVM 은 배열에 접근할 때 마다 경계를 넘지 않는지 검사한다.

    ◦  for 문은 배열의 경계에 도달하면 반복문을 종료한다.

    ◦  for 문 안에서 배열에 접근하고 있으니, 배열의 경계에 대한 검사를 2번 하고 있다고 생각할 수 있다.

    ◦  중복 검사를 피하면 성능을 향상시킬 수 있지 않을까? → 예외를 사용에 제어 흐름을 구현하면 되겠다!

 

    ◦  이 추론이 잘못된 이유

          -  예외는 예외 상황에 쓸 용도로 설계된 것이다. JVM 개발자가 최적화에 신경쓰지 않았을 가능성이 크다.

          -  코드를 try-catch 블록 안에 넣으면 JVM 이 적용할 수 있는 최적화가 제한된다.

          -  배열을 순회하는 for 문은 앞서 걱정한 중복 검사를 수행하지 않는다. JVM 이 알아서 최적화해주기 때문.

          -  결과적으로 성능이 개선되지 않는다! (실제로 코드를 돌려보면 예외를 사용한 쪽이 2배 이상 느리다)

 

    ◦  뿐만 아니라 반복문 안에 버그가 숨어 있다면 흐름 제어에 쓰인 예외가 이 버그를 숨겨 디버깅을 훨씬 어렵게 할 수 있다.

          -  반복문의 몸체 내부에서 다른 배열을 사용하는 경우, 해당 배열에서 ArrayIndexOutOfBound 예외가 발생할 수 있다.

          -  표준 관용구

              -  예외처리 하지 않고 스택 추적 정보를 남긴 다음 해당 스레드를 종료시킨다.

          -  예외를 사용한 반복문

              -  ArrayIndexOutOfBound 예외 발생 = 배열 순회 종료 라고 간주하고 있음

              -  즉 실제 버그 상황에서 발생한 예외를 반복문 종료 상황으로 오해하고 넘어간다.

 

코드를 짤 때는 꼼수를 쓰지 말자

• 표준적이고 쉽게 이해되는 관용구를 사용하라

• 성능 개선을 목적으로 과하게 머리를 쓴 기법은 자제하라.

    ◦  꼼수로 성능이 개선되더라도 자바 플랫폼은 꾸준히 업데이트 되기 때문에 상대적인 성능 우위는 오래가지 않는다.

    ◦  반면 꼼수에 숨겨진 미묘한 버그와 어려워진 유지보수 문제는 계속된다.


상태 검사 메소드 / 옵셔널 / 특정 값

상태 의존적 메소드와 상태 검사 메소드

• 지금까지 살펴본 원칙은 API 설계 시에도 유의해야하는 부분이다.

• 특히 특정 상태에서만 호출할 수 있는 상태 의존적 메소드를 제공하는 클래스의 경우, 상태 검사 메소드를 함께 제공해 사용자가

    예외 처리를 제어 흐름에 사용하지 않도록 해줘야 한다.

    ◦  상태 의존적 메소드 예시 : Iterator 의 next

          -  Iterator 에 다음 원소가 있어야 next 메소드 호출이 가능하므로

    ◦  상태 검사 메소드 예시 : Iterator 의 hasNext

          -  Iterator 에 다음 원소가 있는지 여부를 hasNext 메소드를 통해 검사할 수 있다.

for (Iterator<Foo> i = collection.iterator(); i.hasNext(); ) { Foo foo = i.next(); }

• 상태 검사 메소드 없이 상태 의존 메소드만 있었다면, 사용자는 아래와 같이 코드를 작성할 수 밖에 없을 것이다.

try {
	Iterator<Foo> i = collection.iterator();
	while(true) { Foo foo = i.next(); }
} catch (NoSuchElementException e) {...}

 

 

상태 검사 메소드 / 옵셔널 / 특정 값

• 상태 검사 메소드 대신 올바르지 않은 상태일 때 빈 옵셔널을 반환하거나, null 같이 사전에 정의해둔 특정 값을 반환하는 방법도 있다.

• 아래는 이 세가지 방식 중 하나를 선택하는 것에 대해 책에서 소개하는 지침이다.

    ◦  옵셔널 혹은 특정 값을 반환하는 방식

          -  외부 동기화 없이 여러 스레드가 동시에 접근할 수 있는 경우

          -  외부 요인으로 상태가 변할 수 있는 경우

              → 상태 검사 메소드와 상태 의존적 메소드를 호출하는 사이에 객체의 상태가 변할 수 있기 때문

          -  성능이 중요한 상황, 상태 검사 메서드가 상태 의존적 메서드의 작업 일부를 중복 수행하는 경우

    ◦  상태 검사 메서드를 사용하는 방식

          -  위의 상황을 제외한 다른 모든 경우

              → 가독성이 더 좋다 + 상태 검사 메서드 호출이 누락된 경우 상태 의존적 메소드가 이를 잡아주기 때문에 버그를 잡기 쉽다.

          -  단, 특정 값은 검사하지 않고 지나쳐도 발견하기 어렵다는 단점이 있다.

객체는 클래스가 아닌 인터페이스로 참조하라

클래스가 아닌 인터페이스로 참조하는 예시

• 적합한 인터페이스가 있다면 객체를 클래스 타입이 아닌 인터페이스 타입으로 선언하는 것이 좋다.

    ◦ 이렇게 되면 구체적인 클래스 타입은 객체를 생성할 때 생성자에서만 사용하면 된다

    ◦ 아래는 Set 인터페이스를 구현한 LinkedHashSet 변수를 선언하는 코드이다.

LinkedHashSet implements Set

코드의 유연성을 높이는 설계

• 기존에 사용하던 구현 클래스 타입을 다른 것으로 바꿔야 하는 상황이 종종 있다.

    ◦ 더 성능이 좋은 클래스 타입을 사용하고 싶은 경우

    ◦ 더 다양한 신기능을 사용할 수 있는 클래스 타입이 있는 경우

         -  HashMap 보다 EnumMap 이 속도도 더 빠르고 순회 순서도 키의 순서와 같아 순서 예측이 가능하다.

         -  EnumMap 은 키가 열거 타입인 경우에만 사용할 수 있다. 이 때 LinkedHashMap 을 사용하면 키 타입과 상관없이

            사용할 수 있으면서 순회 순서를 예측할 수 있다.

• 인터페이스 타입으로 객체를 참조했다면 이런 변동사항에 훨씬 유연한 대처가 가능하다.

    ◦ 구현 클래스를 교체하고 싶다면 다른 클래스의 생성자를 호출해주기만 하면 되기 때문

 

• Example

    ◦ LinkedHashSet 대신 HashSet 를 사용하려고 코드를 수정하는 경우

    ◦ Set 인터페이스를 사용한 경우 우변의 생성자 외 부분은 수정이 필요 없다

    ◦ LinkedHashSet 클래스를 사용한 경우 좌, 우변 모두 수정이 필요하다.

 

    ◦ 그러나 누군가는 이렇게 생각할 수 있다

         -  굳이 인터페이스 써야 하나? 그냥 클래스 쓰고 나중에 수정할 때 좌, 우변 다 바꿔주면 되는거 아님?

         -  그러나 이 방법은 자칫하면 컴파일 에러가 발생할 수 있다.

         -  처음에 객체를 인터페이스로 만들었다면, 메소드를 설계할 때 파라미터로 인터페이스 객체를 받도록 유도할 수 있다.

         -  처음에 객체를 클래스 만들었다면, 메소드를 설계할 때 인터페이스가 아닌 클래스 객체를 파라미터로 받게 해버리는 상황이

            발생할 수 있다.

         -  인터페이스를 사용하면 이후 구체 클래스가 바뀌어도 컴파일 문제가 없으나 클래스를 사용하면 구체 클래스가

            바뀔 때 컴파일 문제가 생긴다. (즉 하나가 바뀔 때 수정해야 하는 부분이 더 많아진다 : 유연성 낮음)

         -  즉 애초에 변수를 인터페이스 타입으로 선언해 이 문제를 방지하자.

 

• 주의사항

    ◦ 기존에 사용하던 구현 클래스 타입을 다른 것으로 바꿀 때 유의사항

    ◦ 원래의 클래스가 인터페이스 레벌에서는 없는 특별한 기능을 제공했고, 주변 코드가 이 기능에 의존하여 동작하고 있었다면

      새로운 클래스도 동일한 기능을 제공해야 코드가 깨지지 않는다.

         -  LinkedHashSet 는 반복자의 순회 순서를 보장한다. 이를 염두에 두고 코드를 작성한 상황에서 LinkedHashSet

            HashSet 으로 대체해버리면 연산 오류가 발생할 수 있다.


참조할 만한 인터페이스가 없는 경우

값 클래스 (Value Class)

• 한번 값이 할당 된 이후에 변경되지 않음을 보장해야 하는 경우

    ◦ 예) String, BigInteger 등의 불변 객체

• 값 클래스가 여러가지로 구현될 수 있음을 염두에 두고 설계하는 일이 거의 없다.

• final 인 경우가 많고 상응하는 인터페이스가 별도로 존재하는 경우가 드물다.

• 값 클래스는 매개변수 / 변수 / 필드 / 반환 타입으로 사용해도 무방하다.

 

클래스 기반으로 작성된 프레임워크가 제공하는 객체

• OutputStreamjava.io 패키지의 여러 클래스가 이 부류에 속한다.

 

인터페이스에는 없는 특별한 메소드를 제공하는 클래스

• PriorityQueue 클래스는 Queue 인터페이스에는 없는 comparator 메서드를 제공한다.

• 클래스 타입을 직접 사용하는 경우, 이런 특별 메서드의 사용은 꼭 필요한게 아니라면 최소화하는 것이 좋다.

    ◦ 코드의 유연성을 많이 떨어트리기 때문


정리

객체를 표현할 적절한 인터페이스가 있는지 먼저 찾아보자. 인터페이스로 객체를 참조하면 더 유연한 코드를 작성할 수 있기 때문이다.

만약 없다면, 필요한 기능을 제공해주는 범위 내에서 가장 상위 클래스의 타입을 사용하자.