문제

 

https://school.programmers.co.kr/learn/courses/30/lessons/42583?language=java 

 

프로그래머스

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

programmers.co.kr

문제 설명

트럭 여러 대가 강을 가로지르는 일 차선 다리를 정해진 순으로 건너려 합니다. 모든 트럭이 다리를 건너려면 최소 몇 초가 걸리는지 알아내야 합니다. 다리에는 트럭이 최대 bridge_length대 올라갈 수 있으며, 다리는 weight 이하까지의 무게를 견딜 수 있습니다. 단, 다리에 완전히 오르지 않은 트럭의 무게는 무시합니다.

예를 들어, 트럭 2대가 올라갈 수 있고 무게를 10kg까지 견디는 다리가 있습니다. 무게가 [7, 4, 5, 6] kg인 트럭이 순서대로 최단 시간 안에 다리를 건너려면 다음과 같이 건너야 합니다.


경과 시간 다리를 지난 트럭 다리를 건너는 트럭 대기 트럭
0 [] [] [7,4,5,6]
1~2 [] [7] [4,5,6]
3 [7] [4] [5,6]
4 [7] [4,5] [6]
5 [7,4] [5] [6]
6~7 [7,4,5] [6] []
8 [7,4,5,6] [] []

따라서, 모든 트럭이 다리를 지나려면 최소 8초가 걸립니다.

solution 함수의 매개변수로 다리에 올라갈 수 있는 트럭 수 bridge_length, 다리가 견딜 수 있는 무게 weight, 트럭 별 무게 truck_weights가 주어집니다. 이때 모든 트럭이 다리를 건너려면 최소 몇 초가 걸리는지 return 하도록 solution 함수를 완성하세요.

제한 조건
  • bridge_length는 1 이상 10,000 이하입니다.
  • weight는 1 이상 10,000 이하입니다.
  • truck_weights의 길이는 1 이상 10,000 이하입니다.
  • 모든 트럭의 무게는 1 이상 weight 이하입니다.
입출력 예
bridge_length  weight  truck_weights  return
2 10 [7,4,5,6] 8
100 100 [10] 101
100 100 [10,10,10,10,10,10,10,10,10,10] 110

코드

import java.util.LinkedList;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

class Solution {
	int answer = 1;
	// 트럭 대기열 큐
	LinkedList<Truck> waitingQ;
	// 다리에서 운행중인 트럭 큐
	LinkedList<Truck> bridgeQ;
	Integer sum; // 다리 위 트럭들 총 중량

	public static void main(String[] args) {
		Solution s = new Solution();
		System.out.println(s.solution(2, 10, new int[] {7,4,5,6}));
//		System.out.println(s.solution(100, 100, new int[] {10}));
//		System.out.println(s.solution(100, 100, new int[] { 10, 10, 10, 10, 10, 10, 10, 10, 10, 10 }));

	}
	public int solution(int bridge_length, int weight, int[] truck_weights) {
		// 초기화
		waitingQ = IntStream.range(0, truck_weights.length)
                            .mapToObj( i-> new Truck(truck_weights[i],0) )
                            .collect(Collectors.toCollection(LinkedList::new));
		bridgeQ = new LinkedList<>();
		
		// 루프 탈출은 오롯이 bridgeQ가 비었는지에 의존한다.
		while (true) {
			//목표, 대기큐에 지움과 동시에 대기큐 자리에 바로 채워서 빈 순간이 없게 만들기
			//따라서, 가장 먼저 bridgeQ에서 제거해야 한다.
			
			// 다리 맨 앞 트럭의 달린 거리를 측정해 기준 거리 만큼 달렸으면 지운다.
			if(!bridgeQ.isEmpty() && bridge_length<=bridgeQ.peek().getRunningDistance() ) {
				bridgeQ.remove();
			}
			//지우고 난 뒤(다리를 빠져나옴) 다리위 트럭의 총 무게를 측정한다.
			sum = bridgeQ.stream().map(truck->Integer.valueOf(truck.getWeight()))
                                  .reduce(0, Integer::sum);
			
			//트럭이 나간 만큼 빈자리를 다른 트럭으로 채운다.
			if(!waitingQ.isEmpty()) {
				//총 무게 + 다음에 추가될 트럭 무게가 제한 중량보다 작으면 추가가 가능하다.
				if(sum+waitingQ.peekFirst().getWeight() <= weight ) {
					bridgeQ.add(waitingQ.poll());
				}
			}
			//다리 위 모든 트럭에 달린 거리 1을 추가한다.
			bridgeQ.forEach(Truck::addDistance);
			//총 걸린 시간 계산
			if(!bridgeQ.isEmpty()) answer++;
			//bridgeQ가 비어있다 == 모든 트럭이 지나갔다.
			if(bridgeQ.isEmpty()) break;
			
			print();
		}//while 끝
		return answer;
	}
	
	void print() {
		sum = bridgeQ.stream().map(truck->Integer.valueOf(truck.getWeight())).reduce(0, Integer::sum);
		System.out.println("현재 하중 : "+sum);
		System.out.println("waitingQ"+waitingQ);
		System.out.println("bridgeQ"+bridgeQ);
		System.out.println();
	}
	
	class Truck {
		private int weight;
		private int runningDistance;

		public Truck(int weight, int runningDistance) {
			super();
			this.weight = weight;
			this.runningDistance = runningDistance;
		}

		@Override
		public String toString() {
			return "[weight=" + weight + ", length=" + runningDistance + "]";
		}

		public int getWeight() {
			return weight;
		}

		public void setWeight(int weight) {
			this.weight = weight;
		}

		public int getRunningDistance() {
			return runningDistance;
		}

		public void setRunningDistance(int runningDistance) {
			this.runningDistance = runningDistance;
		}

		public void addDistance() {
			this.runningDistance += 1;
		}
	}
}

결과

현재 하중 : 7
waitingQ[[weight=4, length=0], [weight=5, length=0], [weight=6, length=0]]
bridgeQ[[weight=7, length=1]]

현재 하중 : 7
waitingQ[[weight=4, length=0], [weight=5, length=0], [weight=6, length=0]]
bridgeQ[[weight=7, length=2]]

현재 하중 : 4
waitingQ[[weight=5, length=0], [weight=6, length=0]]
bridgeQ[[weight=4, length=1]]

현재 하중 : 9
waitingQ[[weight=6, length=0]]
bridgeQ[[weight=4, length=2], [weight=5, length=1]]

현재 하중 : 5
waitingQ[[weight=6, length=0]]
bridgeQ[[weight=5, length=2]]

현재 하중 : 6
waitingQ[]
bridgeQ[[weight=6, length=1]]

현재 하중 : 6
waitingQ[]
bridgeQ[[weight=6, length=2]]

8

 

 

'자료구조&알고리즘 > Level2' 카테고리의 다른 글

프린터  (0) 2022.10.21
기능개발  (1) 2022.10.19
위장  (0) 2022.10.08
전화번호 목록  (0) 2022.10.05

코드

//1 가위   2 바위   3 보
public class 가위바위보 {
	//메인 컨셉 , 한쪽으로 이기는 경우의 수만 나열한다.
	//그러면 나머지는 반대 쪽이 이기는 것이다.
	public static void main(String[] args) {
		StringBuilder sb = new StringBuilder();
		int[] a= {1,1,2,3,2,3};
		int[] b= {3,2,1,1,2,3};
		        //A,B,A,B,D,D
		for(int i=0;i<a.length;i++) {
			//비김
			if(a[i]==b[i]) sb.append("D");
			//A가 이기는 경우의 수 전부 나열
			else if(a[i]==1 && b[i]==3 ) sb.append("A");
			else if(a[i]==2 && b[i]==1 ) sb.append("A");
			else if(a[i]==3 && b[i]==2 ) sb.append("A");
			//이외는 B가 이긴 것
			else sb.append("B"); 
		}
		System.out.println(sb);
	}
}

여기서 핵심은 가위바위보가 아니라,  한 쪽에 치우진 경우의 수를 정의하고 그 외 경우는 그와 반대로 처리한다는 이 구조이다.

'자료구조&알고리즘 > 자바(Java) 알고리즘 문제풀이 : 코딩테스트 대비' 카테고리의 다른 글

숫자뒤집기  (0) 2022.11.10
봉우리  (0) 2022.11.09
등수구하기  (0) 2022.11.05
문자뒤집기  (0) 2022.11.03
큰수출력하기  (0) 2022.10.26

지금까지 깨끗한

깨끗한 코드 블록

올바른 함수

이제 높은 차원인 깨끗한 클래스에 대해 다룬다

클래스 체계

관례상 변수 목록이

변수들 중에서도 static public final 이 맨위

그다음 static private

그다음 private

 

그다음 공개 메서드가 나온다

비공개 함수는 자신을 호출하는 공개 함수 직후에 위치

 

따라서 추상화 단계 순서대로 내려간다.

캡슐화

변수와 유틸리티 함수는 가능한 비공개로 두는 것이 좋다.

때로는 protected 선언해 테스트 코드에 접근을 허용하게 한다.

테스트 코드가 함수를 호출하거나 변수를 사용해야 한다면 함수나 변수는 protected public으로 공개한다.

하지만 그 전에 비공개 상태를 유지할 온갖 방법을 강구한다.

캡슐화 풀어주는 결정은 최후의 수단이다

 

클래스는 작아야 한다.

클래스를 만들 번째 규칙은 크기다. 번째도 크기다.

작아야 한다. 작아야 한다.

 

함수와 마찬가지로 작게가 기본규칙이다.

 

얼마나 작게?

함수는 물리적 수로 크기를 측정한다면 클래스는 맡은 책임으로 크기 수준 가늠한다.

맡은 책임은 클래스에 속한 "공개 메서드 수"라고 생각하기 쉽다. 틀린 말은 아니지만 완전히 맞는 말도 아니다.

적은 메서드 수라도 책임이 클 수 있기 때문이다.

 

클래스 작명은 크기를 줄이는 첫 번 단계이다.

만약 작명이 어렵거나 너무 길어진다면 해당 클래스 책임이 너무 많은 것이다.

마찬가지로 클래스 이름이 모호하다면 역시 클래스 책임이 많은 것이다.

Processor, Manager, Super 등과 같은 모호단 단어가 붙는다면 여러 책임을 떠안았다는 표시다.

클래스 설명은 if, and, or, but을사용하지 않고서 25 단어내외로 가능해야한다.

단어들이 붙었다는 것은 여러 책임이 존재한다는 증거이다.

 

단일 책임 원칙 (Single Responsibility Principle : SRP)

클래스나 모듈을 변경할 이유가 하나, 하나뿐이어야 한다는 원칙

SRP 책임이라는 개념을 정의하며 적절한 클래스 크기를 제시한다. 

클래스는 책임, 변경할 이유가 하나여야 한다는 의미다.

책임, 변경할 이유를 파악하려 애쓰다 보면 코드를 추상화하기도 쉬워진다.

 

SRP 객체 지향설계에서 더욱 중요한 개념이다. 또한 이해하고 지키기 수월한 개념이다

그러나 개발자가 가장 무시하는 규칙 하나다.

우리는 수많은 책임을 떠안은 클래스를 꾸준하게 접한다.

 

소프트웨어를 돌아가게 하는 것과 깨끗하게 만드는 것은 완전 별개다

깨끗하고 체계적인 소프트웨어에 초점은 두는게 아니라 프로그램이 돌아가게 하는 것이 나쁜 자세는 아니다.

문제는 거기서 끝이라고 생각하는데 있다. 깨끗하고 체계적인 소프트웨어라는 다음 관심사로 전환하지 않는다.

우리는 반드시 프로그램으로 되돌아가 여러 책임(기능) 가진 클래스를 단일 책임 클래스로 나누어야한다.

 

많은 개발자들이 자잘한 단일 책임 클래스가 많아지면 그림을 이해하기 어려워진다고 우려한다.

그림을 이해하려면 클래스 클래스를 넘나들어야 한다고.

하지만 실제로 작은 클래스가 많은 시스템이던 클래스가 개뿐인 시스템이든 돌아가는 부품은 수가 비슷하다.

그러므로 우리가 고민할 것은 다음과 같다.

도구 상자를 어떻게 관리하고 싶은가?

    작은 서럽을 많이 두고 기능과 이름이 명확하게!!

    아니면 서랍 개를 두고 모두 집어 던져버리기

 

프로젝트는 시스템 논리가 많고 복잡하다. 이런 복잡성을 다루려면 체계적인 정리를 통해 제어하는 것이 중요하다.

그래야 무엇이 어디에 위치한지 알 있다.

또한 변경을 직접 영향이 미치는 컴포넌트만 이해해도 충분하다

큼직한 다목적 클래스로 이뤄진 시스템은 변경을 가할 당장 알필요가 없는 사실까지 들이밀어 방해한다

 

강조한다. 클래스 보다 작은 클래스 여럿으로 이뤄진 시스템이 바람직하다

작은 클래스는 각자 맡은 책임이 하나며, 변경할 이유가 하나며, 다른 작은 클래스와 협력해 시스템에 필요한 동작을 수행한다.

 

응집도(Cohesion)

클래스는 인스턴스 변수 수가 작아야 한다.

클래스 메서드는 클래스 인스턴스 변수 하나 이상 사용해야 한다.

일반적으로 메서드가 변수를 많이 사용할 수록 메서드와 클래스는 응집도가 높다.

특히 모든 인스턴스 변수를 메서드마다 사용하는 클래스는 응집도가 가장 높다.

 

위처럼 응집도가 가장 높은 클래스는 가능하지도 바람직하지도 않다.

우리는 응집도 높든 클래스를 선호한다.

응집도가 높다는 것은 클래스에 속한 메서드와 변수가 서로 의존하며 논리적인 단위로 묶인다

 

'함수를 작게, 매개변수 목록을 짧게' 라는 전략을 따르다 보면 때때로 몇몇 메서드만이 사용하는 인스턴스 변수가 아주 많아진다.

우리는 이제 알고 있다. 이것이 새로운 클래스로 쪼개야 한다는 신호라는 것을

응집도가 높은 클래스를 유지하도록 서로 응집력이 높은 메서드와 변수들을 그룹지어 여러 클래스로 분리해준다.

 

응집도를 유지하면 작은 클래스 여럿이 나온다

함수를 작은 함수 여럿으로 나누기만 해도 클래스 수가 많아진다.

예를 들어, 변수가 많은 함수 하나에서 일부를 작은 함수 하나로 빼내고 싶다.

빼내려는 코드가 함수에 변수 4개를 사용한다. 그러면 작은 함수 인자에 변수 4개를넣어야 하나? 아니다. 이럴 함수에 로컬 변수를 클래스 인스턴스 변수로 만들면 함수는 인수가 필요 없다. 그만큼 함수를 쪼개기 쉬워진다.

다만 이과정에서 불행이도 응집력을 잃는다. 몇몇 함수만 이용하는 인스턴스 변수가 점점 늘어나기 때문이다.

, 이런 상황에서 클래스가 응집력을 잃는다면 새 클래스를 만들어 분리하는 것이다.

그래서 함수를 작은 함수 여럿으로 쪼개다 보면 종종 작은 클래스 여럿으로 쪼갤 기회가 생긴다.

 

리팩터링 과정

  • 원래 프로그램의 정확한 동작을 검증하는 테스트 슈트를 작성
  • 그다음 한 번에 하나씩 수 차례 걸쳐 조금씩 코드를 변경
  • 코드 변경마다 테스트를 수행해 원래 프로그램과 동일하게 동작하는지 확인
  • 이 과정을 반복 정리한 결과로 최종 프로그램 산출

변경하기 쉬운 클래스

대다수 시스템은 지속적인 변경이 가해진다. 과정에서 의도적으로 동작하지 않을 위험이 생긴다. 깨끗한 시스템은 클래스를 체계적으로 정리해 변경에 수반되는 위험을 낮춘다.

 

어떤 변경이던 클래스에 손대면 다른 코드를 망가뜨릴 잠정적인 위험이 있다.

이는 SRP를 위반하는 것이다.

이럴 땐 공통되는 기능을 추상화해 상위 클래스로 만들어 이를 상속하는 작은 클래스로 쪼갠다. 

작은 클래스들은 각기 다른 작은 책임을 수행할 메서드를 구현할 것이다.

추후 변경(수정, 추가)이 발생해도 그 작은 클래스에 국한된다. 

 

위 과정은 객체 지향 설계에서 다른 핵심 원칙인 OCP(Open-Closed Principle)을 지키게 된다.

OCP 클래스는 확장에 개방적이고 수정에 폐쇄적이어야 한다는 원칙이다.

파생 클래스를 생성하는 방식으로 기능에 개방적인 동시에 다른 클래스를 닫아놓는 방식으로 수정에 폐쇄적

 

새 기능을 수정하거나기존 기능을 변경할 건드릴 코드가 최소인 시스템 구조가 바람직하다. 이상적인 시스템이라면 기능을 추가할 시스템을 확장할 기존 코드를 변경하지는 않는다.

 

변경으로부터 격리

요구사항은 변하기 마련이다. 따라서 코드도 변한다.

객체지향 프로그래밍에서 클래스는 구체적인(concreate) 클래스와 추상(abstract) 클래스가 있다

구체적인 클래스는 구현부가 존재하며 추상 클래스는 선언부만 존재한다.

상세한 구현에 의존하는 클라이언트 클래스는 구현이 바뀌면 위험에 빠진다. 따라서 우리는 인터페이스와 추상 클래스를 사용해 구현에 미치는 영향을 격리한다.

 

상세한 구현에 의존하는 코드는 테스트가 어렵다.

 

 시스템의 결합도를 낮추면 유연성과 재사용성도 더욱 높아진다. 결합도가 낮다는 소리는 시스템 요소가 다른 요소로부터 그리고 변경으로부터 격리되어 있다는 의미다. 시스템 요소가 서로 격리되어 있다면 요소를 이해하기도 쉽다.

결합도를 최소로 낮추면 자연스럽게 DIP(Dependency Inversion Principle) 따르는 클래스가 나온다.

DIP 본질은 클래스가 상세한 구현이 아니라 추상화에 의존해야 한다는 원칙이다.

 

'IT책, 강의 > 클린코드(Clean Code)' 카테고리의 다른 글

12장 창발성  (0) 2022.11.05
11장 시스템  (0) 2022.11.01
9장 단위 테스트  (0) 2022.10.21
8장 경계  (0) 2022.10.18
7장 오류 처리  (0) 2022.10.12

코드

import java.util.Arrays;
import java.util.Random;

// 저장소를 두고 나보다 큰 값이 존재할 때 마다 갱신해준다.
public class 큰수출력하기 {
	static int count1 = 0;
	public static void main(String[] args) {
		int count2 = 0;
		//사용한 스트림은 닫힌다.
		
		int[] array = new Random().ints(100000000, 1, 500000000).toArray();
		
		long start = System.nanoTime();
		int maxInt = Arrays.stream(array)//.parallel()
				           .reduce(Integer.MIN_VALUE, (a, b) -> {
				     	      	if(a<b) {
				     	      		System.out.println(a+"   "+b);
				     	      		큰수출력하기.count1++;
				     	      		return b;
				     	      	}else {
				     	      		return a;
				     	      	}
				           	});
		long time1 = System.nanoTime()-start;
		System.out.println("스트림 소요시간 : " + time1);
		System.out.println("=====================");
		
		start = System.nanoTime();
 		int tmp = Integer.MIN_VALUE;
		for(int i=0;i<array.length;i++) {
			if(tmp<array[i]) {
				System.out.println(tmp+"  "+array[i]);
				count2++;
				tmp=array[i];
			}
		}
		long time2 = System.nanoTime()-start;
		System.out.println("반복문 소요시간 : " + time2);
		System.out.println("=====================");
		System.out.println("큰 수1 : "+ maxInt);
		System.out.println("큰 수2 : "+ tmp);
		System.out.println("몇 번 바뀜1? : " + 큰수출력하기.count1);
		System.out.println("몇 번 바뀜2? : " + count2);
		
		System.out.println(time1<time2?"스트림 승":"반복문 승");
		
	}
}

 


결과

-2147483648   476580811
476580811   493765607
493765607   496102572
496102572   498071274
498071274   498579226
498579226   498581580
498581580   499974453
499974453   499982837
499982837   499988721
499988721   499992574
499992574   499997941
499997941   499999160
499999160   499999594
499999594   499999715
499999715   499999907
499999907   499999999
스트림 소요시간 : 183235800
=====================
-2147483648  476580811
476580811  493765607
493765607  496102572
496102572  498071274
498071274  498579226
498579226  498581580
498581580  499974453
499974453  499982837
499982837  499988721
499988721  499992574
499992574  499997941
499997941  499999160
499999160  499999594
499999594  499999715
499999715  499999907
499999907  499999999
반복문 소요시간 : 168843400
=====================
큰 수1 : 499999999
큰 수2 : 499999999
몇 번 바뀜1? : 16
몇 번 바뀜2? : 16
반복문 승
499999779   499999931
499999599   499999629
499999931   499999977
499999814   499999977
499999969   499999977
499999683   499999883
499999961   499999973
499999629   499999916
499999916   499999977
499999983   499999998
스트림 소요시간 : 60819700
=====================
-2147483648  199357576
199357576  398610478
398610478  413062510
413062510  446294614
446294614  464418743
464418743  482929931
482929931  498455214
498455214  499114240
499114240  499685673
499685673  499953566
499953566  499986697
499986697  499989576
499989576  499991638
499991638  499994863
499994863  499995096
499995096  499998840
499998840  499999009
499999009  499999140
499999140  499999772
499999772  499999817
499999817  499999847
499999847  499999983
499999983  499999990
499999990  499999998
반복문 소요시간 : 139373100
=====================
큰 수1 : 499999998
큰 수2 : 499999998
몇 번 바뀜1? : 920
몇 번 바뀜2? : 24
스트림 승

.parallel() 주석을 풀었을 시 결과


스트림과 일반 반복문 차이와 스트림에서 단일과 병렬쓰레드 차이를 보여주고 있다.

 

'자료구조&알고리즘 > 자바(Java) 알고리즘 문제풀이 : 코딩테스트 대비' 카테고리의 다른 글

숫자뒤집기  (0) 2022.11.10
봉우리  (0) 2022.11.09
등수구하기  (0) 2022.11.05
문자뒤집기  (0) 2022.11.03
가위바위보  (0) 2022.10.28

문제

https://school.programmers.co.kr/learn/courses/30/lessons/42587

 

프로그래머스

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

programmers.co.kr

문제 설명

일반적인 프린터는 인쇄 요청이 들어온 순서대로 인쇄합니다. 그렇기 때문에 중요한 문서가 나중에 인쇄될 수 있습니다. 이런 문제를 보완하기 위해 중요도가 높은 문서를 먼저 인쇄하는 프린터를 개발했습니다. 이 새롭게 개발한 프린터는 아래와 같은 방식으로 인쇄 작업을 수행합니다.

1. 인쇄 대기목록의 가장 앞에 있는 문서(J)를 대기목록에서 꺼냅니다.
2. 나머지 인쇄 대기목록에서 J보다 중요도가 높은 문서가 한 개라도 존재하면 J를 대기목록의 가장 마지막에 넣습니다.
3. 그렇지 않으면 J를 인쇄합니다.

예를 들어, 4개의 문서(A, B, C, D)가 순서대로 인쇄 대기목록에 있고 중요도가 2 1 3 2 라면 C D A B 순으로 인쇄하게 됩니다.

내가 인쇄를 요청한 문서가 몇 번째로 인쇄되는지 알고 싶습니다. 위의 예에서 C는 1번째로, A는 3번째로 인쇄됩니다.

현재 대기목록에 있는 문서의 중요도가 순서대로 담긴 배열 priorities와 내가 인쇄를 요청한 문서가 현재 대기목록의 어떤 위치에 있는지를 알려주는 location이 매개변수로 주어질 때, 내가 인쇄를 요청한 문서가 몇 번째로 인쇄되는지 return 하도록 solution 함수를 작성해주세요.

제한사항
  • 현재 대기목록에는 1개 이상 100개 이하의 문서가 있습니다.
  • 인쇄 작업의 중요도는 1~9로 표현하며 숫자가 클수록 중요하다는 뜻입니다.
  • location은 0 이상 (현재 대기목록에 있는 작업 수 - 1) 이하의 값을 가지며 대기목록의 가장 앞에 있으면 0, 두 번째에 있으면 1로 표현합니다.
입출력 예
prioritiesl location  return
[2, 1, 3, 2] 2 1
[1, 1, 9, 1, 1, 1] 0 5
입출력 예 설명

예제 #1

문제에 나온 예와 같습니다.

예제 #2

6개의 문서(A, B, C, D, E, F)가 인쇄 대기목록에 있고 중요도가 1 1 9 1 1 1 이므로 C D E F A B 순으로 인쇄합니다.

 


코드

import java.util.*;
import java.util.stream.Collectors;

class Solution {
	PriorityQueue<Integer> pq = new PriorityQueue<>(Comparator.reverseOrder());
	
	public static void main(String[] args) {
		Solution s = new Solution();
		s.solution(new int[] { 1, 1, 9, 1, 1, 1 },3);
	}

	public int solution(int[] priorities, int location) {
		// 우선순위는 높은 것이 가장 빨리 출력된다. 즉, 내림차순
		pq.addAll(Arrays.stream(priorities).boxed().collect(Collectors.toList()));
		
		System.out.println("입력 받은 인자 : "+Arrays.toString(priorities));
		System.out.println("정렬한    인자 : "+pq);
		
		int order = 0;

		// 우선순위로 정렬한 priorities 큐
		while (!pq.isEmpty()) {
			// priorities 배열 효소를 순회하는 반복문
			for (int i = 0; i < priorities.length; i++) {
				// 우선순위, 즉 큐를 기준으로 priorities배열 요소를 순회한다.
				System.out.println("pq.peek() : "+pq.peek()	+ "   priorities["+i+"] : "+ priorities[i]);
				if (pq.peek() == priorities[i]) {
					if (i == location) {
						order++;
						System.out.println("location : "+location+" 위치 프린트물은 " +order+"번 째로 출력됩니다.");
						return order;
					}
					// 아니면, 순서 증가 및 제일 높은 우선순위를 제거한다.
					order++;
					pq.poll();
				} // if 끝
			} // for 끝
		} // while 끝
		
		return order == 0 ? -1 : order;
	}
	
}

결과

입력 받은 인자 : [1, 1, 9, 1, 1, 1]
정렬한    인자 : [9, 1, 1, 1, 1, 1]
pq.peek() : 9   priorities[0] : 1
pq.peek() : 9   priorities[1] : 1
pq.peek() : 9   priorities[2] : 9
pq.peek() : 1   priorities[3] : 1
location : 3 위치 프린트물은 2번 째로 출력됩니다.

 


이 문제로 큐 동작방식을 이해하는데 도움이 된 것 같다.

 

'자료구조&알고리즘 > Level2' 카테고리의 다른 글

다리를 지나는 트럭  (0) 2022.10.28
기능개발  (1) 2022.10.19
위장  (0) 2022.10.08
전화번호 목록  (0) 2022.10.05

TDD(Test Driven Development) 애자일과 TDD 덕분에 단위 테스트를 자동화하는 프로그래머들이 많아졌다.

이상적인 소스 패키지는 모든 테스트 코드와 소스 코드를 같이 묶은 패키지

TDD 법치 세가지

TDD 실제 코드를 짜기 전에 단위 테스트부터 짜라고 요구한다.

 

가지 법칙

  • 첫째, 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다
  • 둘째, 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다
  • 셋째, 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다

이렇게 일하면 매일 수십 , 매달 수백 , 매년 수천 개에 달하는 테스트 케이스가 나온다.

결국에는 사실상 전부 테스트하는 테스트 케이스가 나온다.

 

깨끗한 테스트 코드 유지하기

실제 코드와 동일한 품질 기준을 가진 테스트 코드가 아니라 빨리 만들어낸 테스트 코드는 결국 테스트 실패로 이어진다.

테스트 코드는 이류 시민이 아니다. 실제 코드 못지 않게 깨끗하게 짜야 한다.

 

테스트는 유연성, 유지보수성, 재사용성을 제공한다

코드에 유연성, 유지보수성, 재사용성을 제공하는 버팀목이 바로 단위 테스트다. 이유는 단순하다.

테스트 케이스가 있으면 변경이 두렵지 않다. 테스트 케이스가 없다면 모든 변경이 잠정적인 버그다. 따라서 테스트 케이스가 없다면 개발자는 변경을 주저한다. 버그가 숨어들까 두렵기 때문이다.

하지만 테스트 케이스가 있다면 두렵지 않다. 테스트 커버리지가 높을 수록 공포는 줄어든다.

 

깨끗한 테스트 코드

깨끗한 코드의 조건 가독성, 명료성, 풍부한 표현력

BUILD-OPERATE-CHECK패턴

  • 첫 부분은 테스트 자료를 만든다.
  • 두 번째 부분은 테스트 자료를 조작한다.
  • 세 번째 부분은 조작한 결과가 올바른지 확인한다

이중 표준

테스트 환경 표준과 실제 코드 표준은 확실히 다르다.

단순하고, 간결하고, 표현력이 풍부해야하는 것은 동일하지만 테스트 환경은 반드시 코드가 효율적일 필요는 없다.(성능)

이것이 이중표준의 본질이다. 실제 환경에서는 절대로 되지만 테스트 환경에서는 문제없는 방식이 있다.

대게 메모리, CPU 효율은효율은 코드의 깨끗함과는 철저히 무관하다.

테스트 assert 하나

JUnit으로 테스트 코드를 때는 함수마다 assert문을 하나만 사용해야 한다는 주장이 있다.

가혹한 규칙이지만 확실한 장점이 있다.

 

이때 함수 작성 시 함수 이름은 given - when - then 관례를 주로 사용한다.

다만 이렇게 분리하면 필연적으로 중복 코드가 많아진다.

 

TEMPLATE METHOD 패턴을 사용하면 중복을 제거할 있다. given/when부분을 부모 클래스 두고 then 부분은 상속을 받은 자식 클래스 두면 된다.

혹은 완전히 독자적인 테스트 클래스를 만들어 @Before함수에 given/when 부분을 넣고 @Test 함수에만 then부분 넣어도 된다.

하지만 결국 모두가 배보다 배꼽이 크다. 이것저것 감안하다보면 assert문을 여럿 사용하는 편이 좋다

이것이 단일 assert 규칙을 부정하는게 아니다. 최대한 지킬려고 하되 여럿 사용하게된다면 최대한 assert 개수를 줄이는 좋다.

 

테스트 개념 하나

사실 "함수당 assert 하나"라는 규칙보다 "테스트 함수마다 개념만 테스트하라" 가 적절한 규칙 같다.

여러 잡다한 개념을 연속으로 테스트 하는 함수는 피한다.

테스트 코드를 보면 assert 여럿 사용하는 것이 문제가 아니라 테스트 여러 개념을 테스트 한다는 사실 핵심 문제이다

따라서 가장 좋은 규칙은 개념 assert 수를 최소로 줄이면서 테스트 함수 하나는 개념 하나만 테스트

 

F.I.R.S.T.

깨끗한 테스트는 다음 다섯 가지 규칙을 따른다.

 

  • Fast 빠르게

테스트는 빨라야 한다.

느리면 자주 돌릴 엄두를 못낸다.

자주 못돌리면 초반에 문제를 찾기 힘들다.

결국 코드 품질을 망치게 된다

 

  • Independent 독립적으로

테스트는 서로 의존하면 안된다.

테스트가 다음 테스트가 실행될 환경을 준비해서는 안된다.

테스트는 독립적으로 어떤 순서로 실행해도 괜찮아야 한다.

테스트가 서로 의존하면 하나가 실패할 나머지도 잇달아 실패하므로 원인 진단이 어려워지며 후반 테스트가 찾아내야 결함이 숨겨진다.

 

  • Repeatable 반복 가능하게

테스트는 어떤 환경에도 반복이 가능해야한다.

실제 환경, QA 환, 네트워크가 연결되지 않은 환경….

테스트가 돌아가지 않는 환경이 하나라도 있다면 테스트가 실패한 이유를 변명이 생긴다.

 

  • Self-Vaildationg 자가 검증하는

테스트는 boolean 값으로 결과를 내야 한다.

, 성공아니면 실패뿐이다. 따라서 결과를 로그 파일을 남겨서 실패인지 성공인지 비교하게 만들어서도 안된다. 로그를 비교하는 순간 주관이 들어가게 된다. 또한 지루한 수작업 평가가 필요하다

 

  • Timely 적시에

테스트는 적시에 작성해야 한다.

단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다.

실제 코드를 구현한 다음에 테스트 코드를 만들면 실제 코드가 테스트하기 어렵다는 사실을 발견할지도 모른다. 또한 테스트가 불가능하도록 실제 코드를 설계할지도 모른다.

 

결론

테스트 코드는 실제 코드만큼이나 프로젝트 건강에 중요하다

테스트 코드는 실제 코드의 유연성, 유지보수성, 재사용성을 보존하고 강화한다.

테스트 코드를 지속적으로 깨끗하게 관리하고 표현력을 높이고 간결하게 정리하자

테스트 APU 구현해 도메인 특화 언어 DSL 만들자. 그러면 그만큼 테스트 코드를 짜기가 쉬워진다.

테스트 코드를 방치하면 실제 코드도 망가진다.

 

 

 

 

'IT책, 강의 > 클린코드(Clean Code)' 카테고리의 다른 글

11장 시스템  (0) 2022.11.01
10장 클래스  (0) 2022.10.26
8장 경계  (0) 2022.10.18
7장 오류 처리  (0) 2022.10.12
6장 객체와 자료 구조  (1) 2022.10.06

문제

문제 설명

프로그래머스 팀에서는 기능 개선 작업을 수행 중입니다. 각 기능은 진도가 100% 일 때 서비스에 반영할 수 있습니다.

또, 각 기능의 개발속도는 모두 다르기 때문에 뒤에 있는 기능이 앞에 있는 기능보다 먼저 개발될 수 있고, 이때 뒤에 있는 기능은 앞에 있는 기능이 배포될 때 함께 배포됩니다.

먼저 배포되어야 하는 순서대로 작업의 진도가 적힌 정수 배열 progresses와 각 작업의 개발 속도가 적힌 정수 배열 speeds가 주어질 때 각 배포마다 몇 개의 기능이 배포되는지를 return 하도록 solution 함수를 완성하세요.

제한 사항
  • 작업의 개수(progresses, speeds배열의 길이)는 100개 이하입니다.
  • 작업 진도는 100 미만의 자연수입니다.
  • 작업 속도는 100 이하의 자연수입니다.
  • 배포는 하루에 한 번만 할 수 있으며, 하루의 끝에 이루어진다고 가정합니다. 예를 들어 진도율이 95%인 작업의 개발 속도가 하루에 4%라면 배포는 2일 뒤에 이루어집니다.
입출력 예

 

progresses  speeds  return
[93, 30, 55] [1, 30, 5] [2, 1]
[95, 90, 99, 99, 80, 99] [1, 1, 1, 1, 1, 1] [1, 3, 2]
입출력 예 설명

입출력 예 #1
첫 번째 기능은 93% 완료되어 있고 하루에 1%씩 작업이 가능하므로 7일간 작업 후 배포가 가능합니다.
두 번째 기능은 30%가 완료되어 있고 하루에 30%씩 작업이 가능하므로 3일간 작업 후 배포가 가능합니다. 하지만 이전 첫 번째 기능이 아직 완성된 상태가 아니기 때문에 첫 번째 기능이 배포되는 7일째 배포됩니다.
세 번째 기능은 55%가 완료되어 있고 하루에 5%씩 작업이 가능하므로 9일간 작업 후 배포가 가능합니다.

따라서 7일째에 2개의 기능, 9일째에 1개의 기능이 배포됩니다.

입출력 예 #2
모든 기능이 하루에 1%씩 작업이 가능하므로, 작업이 끝나기까지 남은 일수는 각각 5일, 10일, 1일, 1일, 20일, 1일입니다. 어떤 기능이 먼저 완성되었더라도 앞에 있는 모든 기능이 완성되지 않으면 배포가 불가능합니다.

따라서 5일째에 1개의 기능, 10일째에 3개의 기능, 20일째에 2개의 기능이 배포됩니다.

 


코드

import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;

class Solution {
    public int[] solution(int[] progresses, int[] speeds) {
    	int before= -1;
    	int count = 0;
    	List<Integer> deployList = new ArrayList<Integer>(); // 배포 기능 수를 담을 리스트
    	
    	int[] remainderArr = IntStream.range(0, speeds.length) // 인덱스 번호를 위한 스트림
    	         .map( i-> requiredDate(progresses[i], speeds[i]))
    	         .toArray();
    	
    	for(int i=0;i<remainderArr.length;i++) {
    		if(i==0)before = remainderArr[i];
    		
    		if(before<remainderArr[i] ) { // 만약 이전 작업보다 내 남은 작업량이 많다면, 이전 기능은 배포될 것이다.
    			deployList.add(count);
                before = remainderArr[i]; // 이제 이 작업이 기준이된다.
    			count=0;				// 배포한 이후 카운터는 초기화
    		}
    		count++;
    		if(remainderArr.length-1 == i) deployList.add(count);
    	}
        return deployList.stream().mapToInt(Integer::intValue).toArray();
    }
    
    private int requiredDate(int progresse, int  speed) {
    	int remainder = 100 - progresse;
    	//나머지가 1이라도 존재하면, 배포도 미뤄진다.
    	return remainder % speed>0? remainder/speed +1 : remainder/speed ; 
    }
}

결과

정확성  테스트
테스트 1 〉	통과 (5.17ms, 76.4MB)
테스트 2 〉	통과 (5.79ms, 85.6MB)
테스트 3 〉	통과 (3.90ms, 80MB)
테스트 4 〉	통과 (4.37ms, 78.4MB)
테스트 5 〉	통과 (4.92ms, 74.8MB)
테스트 6 〉	통과 (2.79ms, 78.2MB)
테스트 7 〉	통과 (4.25ms, 71.3MB)
테스트 8 〉	통과 (3.00ms, 73.7MB)
테스트 9 〉	통과 (3.16ms, 77.2MB)
테스트 10 〉	통과 (2.96ms, 76.3MB)
테스트 11 〉	통과 (4.17ms, 78.3MB)

 


되려 너무 집중을 하면, 마치 당황한 것 마냥 지능이 낮아지는 것 같다. 

이 문제를 푸는 내가 그러했다. 나머지 작업량을 계산하는데, 경곗값을 신경 쓰지 못했다. 

그 하나 때문에 계속 실패했다. 

 

"클린코드" 책에 이런 상황과 적절한 내용이 존재한다.

"경계 조건을 테스트하라"
경계 조건은 각별히 신경 써서 테스트한다. 알고리즘의 중앙 조건은 올바로 짜 놓고 경계 조건에서 실수하는 경우가 흔하다.
"경계 조건을 캡슐화하라"
경계 조건은 빼먹거나 놓치기 십상이다. 경계 조건은 한 곳에서 별도로 처리한다. 코드 여기저기에서 처리하지 않는다. 다시 말해, 코드 여기저기에 +1이나 -1을 흩어놓지 않는다.
"경계를 올바로 처리하지 않는다"
모든 경계 조건, 모든 구석진 곳, 모든 기벽, 모든 예외는 우아하고 직관적인 알고리즘을 좌초시킬 암초다. 스스포의 직관에 의존하지 마라(머릿속에서 코드를 돌려보는 등의 행위) 모든 경계 조건을 찾아내고, 모든 경계 조건을 테스트하라

 

경계 조건에 관한 말만 요약해도 이정도이다. 

 

'자료구조&알고리즘 > Level2' 카테고리의 다른 글

다리를 지나는 트럭  (0) 2022.10.28
프린터  (0) 2022.10.21
위장  (0) 2022.10.08
전화번호 목록  (0) 2022.10.05

모든 소프트웨어를 직접 개발하는 경우는 드물다.

패키지를 사기도 하고, 오픈 소스를 사용하기도 한다. 때로는 사내 컴포넌트를 사용한다.

어떤 식으로든 외부 코드를 우리 코드애 깔끔하게 통합해야만 한다.

장에서는 소프트웨어 경계를 깔끔하게 처리하는 기법과 기교를 살펴본다

 

 

외부 코드 사용하기

인터페이스 , 패키지 , 프레임워크

제공자와 사용자 사이 시스템 경계에서 문제가 발생할 소지가 많다.

 

java.util.map 살펴보면 많은 인터페이스를 제공, 기능성과 유연성을 제공하지만 위험성도 제공한다.

clear() 모든 데이터를 지울수 있다. Map 여기 저기로 넘긴다고 생각해보자.

어딘가에서 누군가가 clear() 호출하지 않으리라 보장할수 있을까?

 또한 Map 저장할 Object타입 객체를 저장하므로 객체유형을 제한하지 않는다.

현재는 지네릭으로 이문제는 없다없다.(지네 릭스는자바 5부터 지원)

 

만약 너무 많은 기능을 제공하거나 래거시 코드가 지네릭을 지원을 안 하는데 적용하고 싶다면

이 또한 래퍼 클래스로 많은 부분을 해결할 수 있다.

예를 들어, Map을 사용하지만 Map의 clear() 호출을 못하게 하고 싶다면

래퍼클래스로 감싸고, clear() 메서드를 호출 못하게 메서드로 만들지 않거나, private으로 만든다.

class Legecy {
	//Map 기능을 제한하고 싶다.
	//public HashMap map = new HashMap();
    Wrapper wrapper = new Wrapper();
}
/*****************/
class Wrapper {
	private HashMap map = new HashMap();
	/*외부에 노출하고 싶은 메서드 정의*/
}

여기서 중요한 것은 Map clear()메서드 따위가 아니다.

Map 같은 유사한 경계 인터페이스를 여기저기 넘기지 말라는 말이다.

 

경계 살피고 익히기

외부 코드 사용의 강점은 적은 시간에 많은 기능을 구현할 있다는

만약 외부코드를 사용하고 싶다면 어디서부터 시작해야할까?

외부 패키지 테스트는 우리 책임이 아니지만 우리 자신을 위해 테스트하는 편이 바람직하다

 

API문서를 먼저 읽으면서 사용법을 결정 이후 예상대로 동작하는 지 확인이 아닌 반대로

우리 쪽코드를 작성해 외부 코드를 호출하는 대신 먼저 간단한 테스트 케이스를 작성해 외부 코드를 익힌다.

이를 학습 테스트라 한다.

학습 테스트는 프로그램에서 사용하려는 방식대로 외부 API 호출한다. , 사용하려는 목적에 초점을 맞춘다.

학습 테스트는이상이다.

학습 테스트에 드는 비용은 없다. 어쨌든 API 배워야하기 때문이다.

오히려 필요한 지식만 확보하는 손쉬운 방법이다.

학습 테스트는이해도를 높여주는 정확한 실험이다

 

패키지가 신규 버전이 나온다면 학습 테스트는 패키지가 예상대로 도는지 검증한다.

패키지가 우리 코드와 호환되리라는 보장이 없기 때문이다.

학습 테스트가 호환되지 않는 코드를 곧바로 밝혀줄 것이다.

 

아직 존재하지 않는 코드를 사용하기

경계와 관련해 다른 유형은 아는 코드와 모르는 코드를 분리하는 경계다.

때로는 우리 지식이 경계를 너머 미치지 못하는 코드 영역도 있다.

때로는 적어도 지금은 알려고 해도 수가 없다.

 

이럴 때 미래 존재할 API 기능을 정의한 인터페이스를 만들고, 작업을 시작한다.

후에 API가 만들어졌으면, ADAPTER 패턴을 이용해 실제 API객체를 사용하도록 만들면 된다.

 

스프링은 기능은 같지만 서로 다른 라이브러리 호출 방법을 하나의 방법으로 호출하기 위해 어댑터 패턴을 사용한다.

 

깨끗한 경계

경계에서는 변경이 자주 이루어진다.

소프트웨어 설계가 우수하다면 변경에 많은 비용이 들지 않는다.

통제하지 못하는 코드를 사용할 때는 너무 많은 투자를 하거나 향후 변경 비용이 지나치게 커지지 않도록 각별히 주의해야한다.

예로 라이브러리는 *.class 내가 코드를 통제 못함

따라서, 감싸기 클래스로 해당 클래스 인스턴스를 생성해 우리 메서드안에서 외부클래스 메서드를 호출

 

경계에 위치하는 코드는 깔끔히 분리한다. 또한 기대치를 정의하는 테스트 케이스도 작성한다.

이쪽 코드에서 외부 패키지를 세세하게 알아야 필요가 없다.

통제가 불가능한 외부 패키지에 의존하는 대신 통제가 가능한 우리 코드에 의존하는 편이 훨씬 좋다.

외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리하자.

Map에서 봤듯이 새로운 클래스로 감싸거나 ADAPTER 패턴을 사용 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환하자

 

'IT책, 강의 > 클린코드(Clean Code)' 카테고리의 다른 글

10장 클래스  (0) 2022.10.26
9장 단위 테스트  (0) 2022.10.21
7장 오류 처리  (0) 2022.10.12
6장 객체와 자료 구조  (1) 2022.10.06
5장 형식 맞추기  (0) 2022.10.05

문제

https://school.programmers.co.kr/learn/courses/30/lessons/1845

 

프로그래머스

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

programmers.co.kr

당신은 폰켓몬을 잡기 위한 오랜 여행 끝에, 홍 박사님의 연구실에 도착했습니다. 홍 박사님은 당신에게 자신의 연구실에 있는 총 N 마리의 폰켓몬 중에서 N/2마리를 가져가도 좋다고 했습니다.
홍 박사님 연구실의 폰켓몬은 종류에 따라 번호를 붙여 구분합니다. 따라서 같은 종류의 폰켓몬은 같은 번호를 가지고 있습니다. 예를 들어 연구실에 총 4마리의 폰켓몬이 있고, 각 폰켓몬의 종류 번호가 [3번, 1번, 2번, 3번]이라면 이는 3번 폰켓몬 두 마리, 1번 폰켓몬 한 마리, 2번 폰켓몬 한 마리가 있음을 나타냅니다. 이때, 4마리의 폰켓몬 중 2마리를 고르는 방법은 다음과 같이 6가지가 있습니다.

  1. 첫 번째(3번), 두 번째(1번) 폰켓몬을 선택
  2. 첫 번째(3번), 세 번째(2번) 폰켓몬을 선택
  3. 첫 번째(3번), 네 번째(3번) 폰켓몬을 선택
  4. 두 번째(1번), 세 번째(2번) 폰켓몬을 선택
  5. 두 번째(1번), 네 번째(3번) 폰켓몬을 선택
  6. 세 번째(2번), 네 번째(3번) 폰켓몬을 선택

이때, 첫 번째(3번) 폰켓몬과 네 번째(3번) 폰켓몬을 선택하는 방법은 한 종류(3번 폰켓몬 두 마리)의 폰켓몬만 가질 수 있지만, 다른 방법들은 모두 두 종류의 폰켓몬을 가질 수 있습니다. 따라서 위 예시에서 가질 수 있는 폰켓몬 종류 수의 최댓값은 2가 됩니다.
당신은 최대한 다양한 종류의 폰켓몬을 가지길 원하기 때문에, 최대한 많은 종류의 폰켓몬을 포함해서 N/2마리를 선택하려 합니다. N마리 폰켓몬의 종류 번호가 담긴 배열 nums가 매개변수로 주어질 때, N/2마리의 폰켓몬을 선택하는 방법 중, 가장 많은 종류의 폰켓몬을 선택하는 방법을 찾아, 그때의 폰켓몬 종류 번호의 개수를 return 하도록 solution 함수를 완성해주세요.

제한사항

  • nums는 폰켓몬의 종류 번호가 담긴 1차원 배열입니다.
  • nums의 길이(N)는 1 이상 10,000 이하의 자연수이며, 항상 짝수로 주어집니다.
  • 폰켓몬의 종류 번호는 1 이상 200,000 이하의 자연수로 나타냅니다.
  • 가장 많은 종류의 폰켓몬을 선택하는 방법이 여러 가지인 경우에도, 선택할 수 있는 폰켓몬 종류 개수의 최댓값 하나만 return 하면 됩니다.

입출력 예

nums  result
[3,1,2,3] 2
[3,3,3,2,2,4] 3
[3,3,3,2,2,2] 2

입출력 예 설명

입출력 예 #1
문제의 예시와 같습니다.

입출력 예 #2
6마리의 폰켓몬이 있으므로, 3마리의 폰켓몬을 골라야 합니다.
가장 많은 종류의 폰켓몬을 고르기 위해서는 3번 폰켓몬 한 마리, 2번 폰켓몬 한 마리, 4번 폰켓몬 한 마리를 고르면 되며, 따라서 3을 return 합니다.

입출력 예 #3
6마리의 폰켓몬이 있으므로, 3마리의 폰켓몬을 골라야 합니다.
가장 많은 종류의 폰켓몬을 고르기 위해서는 3번 폰켓몬 한 마리와 2번 폰켓몬 두 마리를 고르거나, 혹은 3번 폰켓몬 두 마리와 2번 폰켓몬 한 마리를 고르면 됩니다. 따라서 최대 고를 수 있는 폰켓몬 종류의 수는 2입니다.

 


코드


import java.util.stream.IntStream;

class Solution {
	public int solution(int[] nums) {
		int length = IntStream.of(nums)
				 .distinct()
				 .toArray()
				 .length; 
		int choice = nums.length/2;
		return length < choice? length : choice ;
	}
}

결론

정확성  테스트
테스트 1 〉	통과 (3.09ms, 74.8MB)
테스트 2 〉	통과 (2.50ms, 75.1MB)
테스트 3 〉	통과 (3.45ms, 81.5MB)
테스트 4 〉	통과 (6.30ms, 67.1MB)
테스트 5 〉	통과 (2.99ms, 76.5MB)
테스트 6 〉	통과 (2.65ms, 77.7MB)
테스트 7 〉	통과 (2.64ms, 75.1MB)
테스트 8 〉	통과 (4.05ms, 79.3MB)
테스트 9 〉	통과 (3.74ms, 74.9MB)
테스트 10 〉	통과 (5.56ms, 79.5MB)
테스트 11 〉	통과 (7.85ms, 78.7MB)
테스트 12 〉	통과 (5.73ms, 79.4MB)
테스트 13 〉	통과 (5.91ms, 70.8MB)
테스트 14 〉	통과 (5.25ms, 77.4MB)
테스트 15 〉	통과 (32.34ms, 75.4MB)
테스트 16 〉	통과 (15.65ms, 82.3MB)
테스트 17 〉	통과 (13.68ms, 82.8MB)
테스트 18 〉	통과 (23.39ms, 81.8MB)
테스트 19 〉	통과 (18.95ms, 73.5MB)
테스트 20 〉	통과 (7.31ms, 86.1MB)

흠....왜 해시 분류에 이 문제가 있는지 아직 모르겠다.

'자료구조&알고리즘 > Level1' 카테고리의 다른 글

삼총사  (0) 2023.04.19
완주하지 못한 선수  (1) 2022.10.12
숫자 문자열과 영단어  (0) 2022.10.01
성격 유형 검사하기  (0) 2022.09.26
신고 결과 받기  (0) 2022.09.24

오류 코드보다 예외를 사용하라

예외를 지원하지 않는 언어는 오류를 처리하고 보고하는 방법이 제한적이었다.

오류 플래그를 설정하거나 호출자에게 오류 코드를 반환하는 방법이 전부였다.

이 방법은 사용하는 호출자 코드가 복잡해진다.

함수 호출 즉시, 오류를 확인해야 하기 때문이다. 불행히도 단계를 잊어버리기 쉽다.

그래서 오류가 발생하면 예외를 던지는 편이 좋다

그러면 호출자 코드가 깔끔해진다.

논리가 오류 처리 코드와 뒤섞이지 않으니까

핵심은 핵심 알고리즘과 오류 처리 알고리즘이 분리 됐다는

 

Try-Catch-Finally문부터 작성하라

try블록에서 무슨 일이 생기더라도 catch 블록은 프로그램 상태를 일관성 있게 유지해야 한다.

그러므로 예외가 발생할 코드를 짤 때는 무조건 try-catch-finally 문부터 작성하는 게 좋다

 

미확인(unchecked) 예외를 사용하라

현재는 안정적인 소프트웨어를 제작하는 요소로 확인된(Checked) 예외 반드시 필요하지 않다는 사실이 분명해졌다.

확인된(checked) 예외는  OCP(Open Closed Principle) 위반한다.

만약 메서드에서 확인된 예외를 던졌는데 catch블록이 단계 위에 있다면 사이 메서드는 모두 선언부에 해당 예외를 선언부에정의해야 한다. 만약 하위 단계에서 발생 예외가 변경되면 전부 수정해야 할 것이다.

 

대규모 시스템에서 호출이 일어나는 방식을 상상해보라.

최상위 함수가 아래 함수를 호출한다. 아래 함수는 아래 함수를 호출한다. 단계를 내려갈수록 호출하는 함수 수는 늘어난다.

이제 최하위 함수를 변경해 새로운 오류를 던진다고 가정하자.

확인된 오류를 던진다면 함수는 선언부에 throws 절을 추가해야 한다.

그러면 변경한 함수를 호출하는 함수 모두가 catch 블록에서 새로운 예외를 처리하거나 선언부에 throw 절을 추가해야 한다는 말이다.

결과적으로 최하위 단계에서 최상위 단계까지 연쇄적인 수정이 일어난다.

throws 경로에 위치하는 모든 함수가 최하위 함수에서 던지는 예외를 알아야 하므로 캡슐화가 깨진다.

때로는 확인된 예외도 유용하다. 아주 중요한 라이브러리를 작성한다면 모든 예외를 잡아야 한다. 하지만 일반적인 애플리케이션은 의존성이라는 비용이 이익보다 크다.

 

예외에 의미를 제공하라

예외를 던질 때는 전후 상황을 충분히 덧붙인다. 그래야 오류가 발생한 원인과 위치를 찾기가 쉬워진다.

자바는 모든 예외에 호출 스택을 제공한다. 하지만 실패한 코드의 의도를 파악하려면 호출 스택만으로 부족하다.

 

오류 메시지에 정보를 담아 예외와 함께 던진다. 메시지에 실패한 연산 이름과 실패 유형도 언급한다. 혹은 로깅 기능을 사용한다면 catch에서 충분한 정보를 가진 로그에 남기는 게 좋다.

호출자를 고려해 예외 클래스를 정의하라

오류를 분류하는 방법은 수없이 많다.

오류가 발생한 위치로 분류가 가능하다.

예시, 오류가 발생한 컴포넌트로 분류한다

유형으로도 분류가 가능하다

, 디바이스 실패, 네트워크 실패,…

오류를 정의할 가장 중요한 관심사는 오류를 잡아내는 방법이 되어야 한다.

예를 들어, API를 사용하는데, API가 발생시킬 수 있는 예외를 전부 catch 문으로 나열했다.

catch 문을 처리하는 방법이 거의 동일하다면, API를 감싼 래퍼 클래스를 만들어 하나의 예외로 변환하는 것을 들 수 있다.

public class 예시 {
	외부api api = new 외부api();
	try {
		api.work();
		//예외처리가 비슷하다.
	}catch(외부api예외1 e1) {
		logger.log(e1.getMessage());
	}catch(외부api예외2 e2) {
		logger.log(e1.getMessage());
	}catch(외부api예외3 e3) {
		logger.log(e1.getMessage());
	}
	
}
/* ********************************* */
class 예시 {
	래퍼클래스 api = new 래퍼클래스();
	try {
		api.work();
	}catch(변환된예외 e1) {
		logger.log(e1.getMessage());
	}
	
}
class 래퍼클래스{
	외부api api;
	
	void work() {
		try {
			api.work();
			//예외처리가 비슷하다.
		}catch(외부api예외1 e1) {
			throw new 변환된예외(e1);
		}catch(외부api예외2 e2) {
			throw new 변환된예외(e2);
		}catch(외부api예외3 e3) {
			throw new 변환된예외(e3);
		}
	}
}

실제로 외부 API 사용할 때는 감싸기 기법이 최선이다.

외부 API 감싸면 외부 라이브러리와 프로그램 사이에서 의존성이 크게 줄어든다.

나중에 다른 라이브러리로 갈아타도 비용이 적다.

또한 감싸기 클래스에서 외부 API 호출하는 대신 테스트 코드를 넣어주는 방법으로 프로그램을 테스트하기도 쉬워진다.

 

마지막 장점으로 감싸기 기법을 사용하면 특정 업체가 API 설계한 방식에 발목 잡히지 않는다.

프로그램이 사용하기 편리한 API 정의하면 그만이다.

 

흔히 예외 클래스가 하나만 있어도 충분한 코드가 많다. 예외 클래스에 포함된 정보로 오류를 구분해도 괜찮은 경우가 그렇다.

null 반환하지 마라

오류를 반환하는 흔한 습관 하나다.

null 반환한다는 것은 일거리가 늘 뿐만 아니라 호출자에게 책임을 전가하는 방법이다. 문제는어디서 인가 null체크를 번이라도 빼먹어서 NullPointerException 발생한다면 도대체 어디서 NullPointerException 찾아 처리해야 할까?

null 반환할 바에 예외를 던지거나 특수 사례 객체를 반환 단다.

//특수사례객체, 예외 던지기 대신 사용할 수 있는 방법
List find() {
	if(찾는게 없다...) {
		return Collections.EMPTY_LIST;
	}
}

클래스를 만들거나 객체를 조작해 특수 사례를 처리하는 방식

이점은 클라이언트 코드가 예외적인 상황을 처리할 필요가 없어진다.

클래스나 객체가 예외적인 상황을 캡슐화해서 처리하니까

null 전달하지 마라

null 반환하는 것보다 나쁜 것이 메서드에 null 인자로 넘기는 것이다.

정상적인 인수로 null 기대하는 API 아니라면 메서드로 null 전달하는 코드는 최대한 피한다.

null 체크하기보다 애초에 null이 안 넘어오는 게 정상적인 것이다

결론

깨끗한 코드는 일기도 좋아야 하지만 안정성도 높아야 한다.

오류처리를 프로그램 논리와 분리해 독자적으로 처리해야 튼튼하고 깨끗한 코드가 된다.

오류처리를 프로그램 논리와 분리하면 독립적인 추론이 가능해져 코드 유지보수성도 크게 높아진다.

'IT책, 강의 > 클린코드(Clean Code)' 카테고리의 다른 글

9장 단위 테스트  (0) 2022.10.21
8장 경계  (0) 2022.10.18
6장 객체와 자료 구조  (1) 2022.10.06
5장 형식 맞추기  (0) 2022.10.05
4장 주석 - 나쁜 주석  (1) 2022.10.01

+ Recent posts