학교 수업에서 데이터를 입력받을 때, Scanner 객체를 만들어서 사용했었다. 근데 최근 알고리즘 문제를 풀다보니 많은 사람들이 BufferedReader 클래스를 사용하는 것을 발견했다. 둘의 차이점은 뭐가 있는지 알아보고자 한다.

 

사용하는 방법

Scanner 객체 만들기

Scanner scanner = new Scanner(System.in);

생각보다 Scanner 객체를 만드는 것은 간단하다. System.in을 생성자 파라미터로 넣어서 생성해주면 된다. 이때 Scanner를 사용하기 위해서는 java.util를 import 해줘야 합니다.

 

BufferedReader 객체 만들기

BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));

BufferedReader 객체도 System.in을 파라미터로 받긴 하지만, 조금 다른 점이 있다. "InputStreamReader" 이 친구는 뭐하는 친구일까 한번 알아보자.

 

InputStreamReader란?

문자열을 읽을 때 Character 단위로 한 글자씩 읽어들이는 클래스라고 한다. InputStreamReader를 이용했을 때, 긴 문자열을 읽어들이는 것은 상당히 비효율적이기 때문에 BufferedReader가 생겼다고 한다.

 

여기서 BufferedReader의 특징이 하나 나타난다. Character 단위로 읽어들이던 InputStreamReader의 단점을 보완하기 위해서 "버퍼"를 이용한다. 

 

한 글자씩 읽을 때는 사용자의 요청이 들어올 때마다 데이터를 읽어와야했었기 때문에 긴 문장이 들어오면 상당히 번거로웠다. 이때 BufferedReader를 사용하면 일정한 크기의 데이터를 한번에 읽어와서 "버퍼"에 보관을 합니다. 그리고 사용자의 요청이 들어오게 되었을 때, 버퍼에서 데이터를 읽어오는 방식을 사용합니다.

 

이런 특징으로 BufferedReader의 경우 InputStreamReader를 사용할 때보다 속도도 빠르고, 시간적인 부하도 적게 걸린다고 한다. 하지만 BufferedReader를 사용하면 입력을 Line 단위로 받기 때문에 공백(" ")의 경우도 String으로 받아들이게 됩니다.

 

BufferedReader로 입력받아서 사용하는 데이터는 String 타입의 데이터이기 때문에 만약 정수형이나 실수형 등 다른 데이터 타입으로 사용하고 싶다면 형 변환이 필요하다는 특징이 있습니다.

 

 

비교

그럼 Scanner와 BufferedReader에는 무슨 차이가 있을까. 앞서 설명했던 BufferedReader의 특징을 살펴보면, Line 단위로 데이터를 받아들이고, 입력된 데이터의 타입은 모두 String 타입이다. 그래서 항상 다양한 데이터로 활용하기 위해서는 형 변환이 필요하다. 

 

반면, Scanner의 경우 공백, 줄바꿈을 모두 입력의 경계로 판단을 하기 때문에 좀 더 쉽게 데이터를 입력받을 수 있습니다. 또한 데이터를 입력받을 때, 데이터의 타입을 결정하기 때문에 별도의 형 변환이 필요 없다는 특징이 있습니다. 그럼 다 Scanner 쓰는게 좋은거 아닌가라는 생각이 들었다. 왜 Scanner를 놔두고 BufferedReader를 사용하는 걸까.

 

BufferedReader와 Scanner를 비교한 표

 

위에 있는 표를 보면 BufferedReader의 경우 8192로 Scanner보다 더 많은 버퍼의 크기를 가지고 있습니다. 그리고 단순히 문자열 자체를 읽어들이는 BufferedReader와 다르게 Sacnner는 문자열 파싱이 가능합니다. 그래서 상대적으로 속도는 BufferedReader가 더 빠릅니다.

 

또 다른 특징으로는 BufferedReader는 멀티 쓰레드 환경에서 동기화(Syncronized)가 되기 때문에 더 안전하다고 한다. 이 부분은 아직 겪어보지 않았기 때문에 생략을 하도록 하겠습니다.

 

그리고 BufferedReader를 사용하게되면 IOException에 대해서도 처리를 해줘야 합니다. 숫자 2개를 입력받아서 더한 값을 출력하는 메서드인 plus()를 한번 Scanner와 BufferedReader를 이용해서 구현해보겠습니다.

 

BufferedReader를 이용하여 구현

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

public class BufferedReaderEx {
    public static void main(String[] args) throws IOException {
        int result = plus();

        System.out.println(result);
    }

    public static int plus() throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));

        int firstValue = Integer.parseInt(bufferedReader.readLine());
        int secondValue = Integer.parseInt(bufferedReader.readLine());

        bufferedReader.close();

        return firstValue + secondValue;
    }
}
입력
1
3

출력
4

위에서 BufferedReader를 이용할 때의 특징을 확인해볼 수 있다.

 

  • IOException을 던지기 때문에 메서드 차원에서 throws IOException을 처리해준 부분
    • 이 부분은 try-catch로 잡아서 처리해도 무방합니다. 

  • 입력된 데이터를 직접 파싱하여 정수형(int) 데이터로 사용하기
    • Integer.parseInt()를 사용하여 문자열로 입력된 데이터인 "1"과 "3"을 정수형 데이터인 1과 3으로 변경하여 사용한다.

 

 

Scanner를 이용하여 구현

import java.util.Scanner;

public class ScannerEx {
    public static void main(String[] args) {
        int result = plus();

        System.out.println(result);
    }

    public static int plus() {
        Scanner scanner = new Scanner(System.in);

        int firstValue = scanner.nextInt();
        int secondValue = scanner.nextInt();

        scanner.close();

        return firstValue + secondValue;
    }
}
입력
1
3

출력
4

Scanner 객체를 이용하여 만든 코드가 더 간단해 보이는 것은 사실이다. 

 

  • IOException을 던지지 않기 때문에 예외를 처리해줄 필요가 없다.

  • 문자열에 대한 파싱을 제공하기 때문에 scanner.nextInt()를 사용하면 별도의 파싱 작업을 해줄 필요가 없이 정수형 데이터로 사용할 수 있다.

 

어떤 것을 사용하는지는 본인의 선택이겠지만, 적은 데이터를 입력받을 때는 Scanner와 BufferedReader에서의 큰 차이를 못 느낄 것이다. 하지만 데이터의 양이 정말 많아진다면 문자열 파싱과 같은 복잡한 과정을 거치지 않고, 버퍼의 크기가 더 큰 BufferedReader의 속도가 빠르다는 것은 이제 알 것 같다. 

 

 

Reference

https://carpediem0212.tistory.com/11

 

BufferedReader와 Scanner

이번 포스팅에서는 Java에서 문자열을 입력받는 목적으로 많이 사용되는 BufferedReader 클래스와 Scanner 클래스에 대해 알아보려합니다. 두 Class 모두 문자열을 입력 받는데 사용된다는 공통점이 있

carpediem0212.tistory.com

 

싱글톤 패턴은 디자인 패턴 중에서 가장 자주 사용되는 패턴이기도 하면서 가장 많은 비판을 받는 패턴이다. 이런 싱글톤 패턴은 매우 조심해서 사용해야 하거나 피해야 할 패턴이라고 말하기도 한다.

 

스프링 공부를 하면서 싱글톤이라는 패턴 용어를 보게 되었고, 정리해보자는 생각에 작성하게 됩니다.

 

싱글톤 패턴이란 무엇인가?

객체의 인스턴스가 오직 하나만 존재하도록 강제하는 패턴을 말한다. 이렇게 하나만 만들어지는 클래스의 오브젝트는 애플리케이션 내에서 전역적으로 접근이 가능하다. 단일 오브젝트만 존재해야 하고, 이를 애플리케이션의 여러 곳에서 공유하는 경우에 주로 사용하는 패턴이다.

 

 

자바 코드로 확인

자바에서는 싱글톤패턴을 구현하는 방법은 다음과 같다.

 

  1. 클래스 밖에서는 오브젝트를 생성하지 못하도록 생성자를 private로 만든다.
  2. 생성된 싱글톤 오브젝트를 저장할 수 있는 자신과 같은 타입의 스태틱 필드를 정의한다.
  3. 스태픽 팩토리 메서드인 getInstance()를 만들고 이 메서드가 최초로 호출되는 시점에서 한 번만 오브젝트가 만들어지게 만든다. 생성된 오브젝트는 스태틱 필드에 저장된다. 또는 스태틱 필드의 초기값으로 오브젝트를 미리 만들어둘 수도 있다.
  4. 한번 오브젝트가 만들어지고 난 후에는 getInstance() 메서드를 통해 이미 만들어져 있는 스태틱 필드에 저장해둔 오브젝트를 넘겨준다.
public class Singleton {
    public static Singleton instance = new Singleton();
    
    private Singleton() { }; // 외부에서 객체를 생성하지 못하도록 설정
    
    public static Singleton getInstance() {
    	return instance;
    }
}

Singleton이라는 클래스는 오직 하나의 instance만을 갖도록 만든다. 생성자는 외부에서 객체를 생성하지 못하도록 private로 설정한다. 만들어진 인스턴스를 가져오고 싶다면 getInstance() 메서드를 사용하여 오직 하나의 instance만을 사용할 수 있도록 한다.

 

 

싱글톤 패턴의 특징

싱글톤 패턴은 오직 한 번의 new 연산자를 사용하기 때문에 메모리 측면에서 이점을 가져올 수 있다. 그리고 이미 만들어져 있는 인스턴스를 사용하기 때문에 속도 차원에서도 이득을 얻을 수 있다.

 

그리고 인스턴스가 오직 하나만 있기 때문에 전역적으로 해당 인스턴스에 접근을 할 수 있어 데이터를 공유하기 쉽다는 것이다. 하지만 데이터 공유에 대한 이점도 있지만 만약 동시에 여러 곳에서 해당 인스턴스에 접근하여 사용하게 되었을 때, 동시성 문제가 발생할 수 있다는 단점이 있다.

 

자원을 공유하기 때문에 발생하는 문제는 테스트하기 어렵다는 것이다. 완벽히 격리된 상태에서 테스트하기 위해서는 매번 인스턴스를 초기화해주는 작업을 진행해야 한다. 그렇지 않으면 해당 인스턴스가 전역적으로 사용되기 때문에 원활한 테스트를 진행할 수 없다.

 

이외에도 내부 상태를 변경하기 힘들고, 자식 클래스를 만들 수 없다는 문제점이 존재한다. 자식 클래스를 만들 수 없다는 것은 확장에 닫혀있다는 것을 의미하고, 객체지향 SOLID 원칙 중에서 OCP에 위배된다는 것을 의미한다. 즉, 싱글톤 패턴의 클래스는 유연성이 많이 떨어지는 문제점이 존재한다.

 

하지만 스프링 프레임워크의 도움을 받게되면 싱글톤 패턴의 문제점을 보완하면서 장점의 혜택을 누릴 수 있다. 

 

reference

싱글톤(Singleton) 패턴이란?

토비의 스프링

Java를 공부하다 보니 상속을 통한 확장성에 대한 얘기를 많이 접해볼 수 있었다. 그중에서도 지금 공부하고 있는 스프링 프레임워크에서 많이 사용하고 있는 디자인 패턴인 "템플릿 메서드 패턴"에 대해서 짧게 적어보려고 한다.

 

먼저 디자인 패턴이란 무엇인가? 소프트웨어 설계를 할 때 자주 만나는 문제들이 존재한다. 반복되는 문제들을 해결할 수 있는 해결방안이 정해져 있으면 좋지 않을까? 그래서 있는 존재하는 것이 디자인 패턴입니다.

 

즉, 디자인 패턴이란 소프트웨어 설계 시 특정 상황에서 자주 만나게 되는 문제를 해결하기 위해 사용할 수 있는 재사용이 가능한 솔루션을 의미한다.

 

이제 템플릿 메서드 패턴에 대해서 알아봅시다. 템플릿 메서드의 특징은 다음과 같습니다.

1. 추상 메서드, 구현된 메서드를 이용하여 코드의 흐름을 정의하는 메소드
2. 메서드를 final로 선언하여 하위 클래스에서 재정의할 수 없게 함
3. 프레임워크에서 많이 사용되는 설계 패턴
4. 추상 클래스로 선언되어 있는 상위 클래스에서 템플릿 메서드를 사용하여 전체적인 흐름을 정의하고, 하위 클래스에서 다르게 구현되어야 할 부분은 추상 메서드로 선언하여 하위 클래스에서 정의하도록 함

쉽게 풀어보면 상위 클래스에서 실행되어야 할 코드의 흐름(시나리오)을 정의하고 있는 final 메서드를 선언합니다. 이 메서드를 템플릿 메서드라고 합니다. 

 

그리고 해당 추상 클래스를 상속받은 하위 클래스에서는 상위 클래스의 추상 메서드를 오버라이딩하여 정의하여 사용합니다. 하위 클래스는 상위 클래스의 템플릿 메서드를 재정의할 수 없습니다. 왜냐하면 final 키워드가 붙어있기 때문입니다.

 

코드로 된 예시를 하나 살펴보면서 설명해 보겠습니다.

TV를 예시로 들어봅시다. TV는 다음과 같은 고정된 시나리오로 코드가 실행된다고 해봅시다.

TV를 켜고 -> (TV가 추천 채널을 찾는다) -> TV가 채널을 선택 -> 해당 채널이 재생 -> TV가 꺼짐

추천 채널을 찾는 부분은 ()를 쳐놓은 이유는 구현을 해도 되고, 안 해도 되는 메서드입니다. 그럼 이 메서드는 추상 메서드인가? 아닙니다. 추상 메서드가 아닌 구현이 되어있지만 비어있는 메서드입니다. 이런 메서드를 훅 메서드라고 합니다.

 

이제 TV를 상속받는 SmartTV와 ManualTV를 구현해 봅시다.

 

 

# 추상 클래스인 TV를 생성

TV는 코드 흐름을 가지고 있는 run()이라는 템플릿 메서드를 가지고 있습니다.

public abstract class TV {
    public abstract void turnOn();
    
    public abstract void turnOff();
    
    public abstract void selectChannel();
    
    // 훅 메서드
    public void findChannel() {};

    public void display() {
        System.out.println("채널이 재생됩니다.");
    };

    // 템플릿 메서드
    public final void run() {
        turnOn();        // TV가 켜지고
        findChannel();   // 채널을 찾고
        selectChannel(); // 채널을 선택하고
        display();       // 해당 채널이 재생된다.
        turnOff();       // TV를 끈다.
    }

}

템플릿 메서드인 run()을 보면 final로 선언되어 있고, 코드의 흐름이 정의되어 있습니다. 여기서 훅 메서드인  findChannel()을 확인해 보면 추상 메서드가 아니고, 구현되어 있는 메서드입니다. 하지만 내용이 비어있습니다.

 

즉, 하위 클래스에서 재정의해주면 이 기능을 사용할 수 있고, 재정의해주지 않으면 해당 기능은 그냥 아무것도 실행되지 않고 지나갑니다.

 

 

# SmartTV 클래스를 생성

스마트 티비는 자동이라는 기능으로 모든 기능을 정의할 것이고, ManualTV와는 다르게 훅 메서드인 findChannel()도 재정의해서 사용할 것입니다.

public class SmartTV extends TV {
    @Override
    public void turnOn() {
        System.out.println("Smart TV가 자동으로 켜집니다.");
    }

    @Override
    public void turnOff() {
        System.out.println("Smart TV가 자동으로 꺼집니다.");
    }

    @Override
    public void findChannel() {
        System.out.println("Smart TV가 추천 채널을 탐색합니다.");
    }

    @Override
    public void selectChannel() {
        System.out.println("Smart TV가 추천 채널을 자동으로 선택합니다.");
    }
}

 

 

# ManualTV 클래스 생성

매뉴얼 티비는 수동으로 작동하기 때문에 리모컨으로 작동되는 내용으로 모든 기능을 정의할 것이고, 자동으로 채널을 찾는 기능인 findChannel()은 구현하지 않을 것입니다.

public class ManualTV extends TV {
    @Override
    public void turnOn() {
        System.out.println("리모컨으로 TV를 켭니다.");
    }

    @Override
    public void turnOff() {
        System.out.println("리모컨으로 TV를 끕니다.");
    }

    @Override
    public void selectChannel() {
        System.out.println("리모컨으로 보고싶은 채널을 선택합니다.");
    }
}

 

 

# 각 TV에 있는 템플릿 메서드를 실행

SmartTV와 ManualTV의 각 run() 메서드를 실행시켜 봅니다.

public class TVTest {
    public static void main(String[] args) {
        TV smartTV = new SmartTV();
        TV manualTV = new ManualTV();

        System.out.println("스마트 티비를 실행");
        smartTV.run();

        System.out.println("매뉴얼 티비를 실행");
        manualTV.run();
    }
}

출력 결과는 아래와 같습니다.

스마트 티비를 실행
Smart TV가 자동으로 켜집니다.
Smart TV가 추천 채널을 탐색합니다.
Smart TV가 추천 채널을 자동으로 선택합니다.
채널이 재생됩니다.
Smart TV가 자동으로 꺼집니다.

매뉴얼 티비를 실행
리모컨으로 TV를 켭니다.
리모컨으로 보고싶은 채널을 선택합니다.
채널이 재생됩니다.
리모컨으로 TV를 끕니다.

스마트 티비에서는 훅 메서드인 findChannel()이 작동되었고, 매뉴얼 티비에서는 작동되지 않은 결과를 볼 수 있습니다. 

 

그리고 2개의 TV는 템플릿 메서드로 정해진 코드 흐름(시나리오)대로 실행됩니다. 이 템플릿 메서드는 final 메서드이기 때문에 SmartTV, ManualTV에서 재정의할 수 없습니다.

 

여기서 final에 대해서 간단하게 확인하고 넘어가 봅시다.

 

# final이 변수에 붙었을 때

private static final int ZERO = 0;
private static final int ONE = 1;

변수에 final이 붙은 경우에는 상수로서 사용됩니다. 상수는 값을 변경할 수 없습니다. 

우리가 자주 사용하는 상수에는 Integer 클래스의 MAX_VALUE가 있습니다.

 

 

# fina이 메서드에 붙었을 때

public final void templateMethod() {
    // 코드 구현
}

메서드에 final이 붙은 경우에는 해당 메서드는 재정의될 수 없습니다. 위에서 살펴봤던 템플릿 메서드가 이 경우에 해당합니다.

 

 

# final이 클래스에 붙었을 때

public final class TV {
    // 코드 구현
}

클래스에 final이 붙은 경우에는 해당 클래스는 상속할 수 없습니다. 즉, extends TV가 불가능합니다.

직면한 문제

기존에 h2 데이터베이스를 썼을 때, 사용했던 설정에서는 data-h2.sql 파일이 mysql에는 적용되지 않음.

 

 

MySQL 연동하기

dependencies {
	implementation 'mysql:mysql-connector-java'
}

먼저 mysql에 대한 의존성을 추가해주고, build.gradle을 refresh해서 적용시켜줍니다.

 

 

해결 방법

mysql을 이용하기 위한 application.yml 설정을 먼저 해줍니다.

spring:
    datasource:
        url: jdbc:mysql://localhost:3306/{스키마 이름}?serverTimezone=UTC&characterEncoding=UTF-8
        driver-class-name: com.mysql.cj.jdbc.Driver
        username: {사용자이름}
        password: {비밀번호}

여기까지 설정해주면 기존에 h2 데이터베이스에서 실행되던 모든 것들이 mysql에서 실행이 됩니다. 테이블 생성까지는 모두 적용이 됐지만, 기존에 있는 data-h2.sql에 있는 초기 데이터 적용을 위한 sql 문이 실행이 되지 않았습니다.

INSERT INTO posts (created_date, modified_date, title, content, author) VALUES (now(), now(), '제목1', '본문1', '작성자1');
INSERT INTO posts (created_date, modified_date, title, content, author) VALUES (now(), now(), '제목2', '본문2', '작성자2');

위 코드는 data-h2.sql 파일 안에 있는 내용입니다. 이제 우리는 h2 데이터베이스가 아니고 mysql을 사용하기 때문에 파일이름을 먼저 data.sql로 바꿔줬습니다.

 

그 다음 아래와 같은 application.yml 설정을 추가해줬습니다.

  sql:
    init:
      mode: always

  jpa:
    defer-datasource-initialization: true
    hibernate:
      ddl-auto: create-drop

sql.init.mode.always는 모든 데이터베이스에 sql 스크립트를 적용하겠다는 설정입니다. 다음으로 jpa.defer-datasource-initialization.true는 h2 데이터베이스 게시글에도 설명했듯이 springboot 2.5 버전 이상부터는 data.sql 스크립트는 Hibernate가 초기화 되기 전에 실행된다고 한다. Hibernate가 초기화 된 후 sql 스크립트를 적용하기 위한 설정입니다.

 

data.sql에 있는 sql 스크립트가 서버를 돌렸을 때, 데이터가 초기화되는 것을 확인할 수 있습니다.

[인증평가(5차) 기출] 업무 처리 문제 보러가기

 

Softeer

연습문제를 담을 Set을 선택해주세요. 취소 확인

softeer.ai

 

문제는 위 링크에 접속하면 직접 문제를 풀어보실 수 있습니다!

 

문제 설명

어떤 부서의 업무 조직은 완전이진트리 모양이다. 즉, 부서장이 루트이고 부서장 포함 각 직원은 왼쪽과 오른쪽의 부하 직원을 가진다. 부하 직원이 없는 직원을 말단 직원이라고 부른다.

모든 말단 직원은 부서장까지 올라가는 거리가 동일하다. 조직도 트리의 높이는 H이다. 아래는 높이가 1이고 업무가 3개인 조직도를 보여준다.

 

업무는 R일 동안 진행된다. 처음에 말단 직원들만 각각 K개의 순서가 정해진 업무를 가지고 있다. 각 업무는 업무 번호가 있다. 각 날짜에 남은 업무가 있는 경우, 말단 직원은 하나의 업무를 처리해서 상사에게 올린다. 다른 직원들도, 대기하는 업무가 있는 경우 업무를 올라온 순서대로 하나 처리해서 상사에게 올린다. 단, 홀수 번째 날짜에는 왼쪽 부하 직원이 올린 업무를, 짝수 번째 날짜에는 오른쪽 부하 직원이 올린 업무를 처리한다.

부서장이 처리한 일은 완료된 것이다. 업무를 올리는 것은 모두 동시에 진행한다. 따라서 그날 올린 업무를 상사가 처리하는 것은 그 다음날에야 가능하다.

부서의 조직과 대기하는 업무들을 입력 받아 처리가 완료된 업무들의 번호의 합을 계산하는 프로그램을 작성하라.

제약조건

1 ≤ H ≤ 10
1 ≤ K ≤ 10
1 ≤ R ≤ 1,000

입력형식

첫 줄에 조직도의 높이 H, 말단에 대기하는 업무의 개수 K, 업무가 진행되는 날짜 수 R이 주어진다.

그 다음 줄부터 각각의 말단 직원에 대해 대기하는 업무가 순서대로 주어진다.

제일 왼쪽의 말단 직원부터 순서대로 주어진다.

출력형식

완료된 업무들의 번호 합을 정수로 출력한다.

입력예제1
1 1 1
1
2
출력예제1
0
입력예제2
1 3 2
9 3 7
5 11 2
출력예제2
5

👨🏻‍💻 생각해보기

1. 말단 직원과 상사의 남은 일은 다르게 처리되어야 할 거 같다는 생각을 했다.

  • 말단 직원은 그냥 자신에게 남겨져있는 일을 하루에 하나씩 상사에게 올려주기만 하면된다.
  • 상사는 왼쪽 직원이 올려준 일, 오른쪽 직원이 올려준 일을 분류해서 처리해야 한다.
    • 날짜가 1일이라면 홀수번째(왼쪽) 직원이 준 일을 처리해서 상사에게 올려야한다.
    • 날짜가 2일이라면 짝수번째(오른쪽) 직원이 준 일을 처리해서 상사에게 올려야한다.

2. 들어온 일의 순서대로 처리되어야 하고, 말단과 상사를 구분해야 한다?

  • 들어온 일을 순서대로 처리하기 위해서는 Queue를 사용하기로 했다.
  • 직원 객체를 만들어서 사용했고, 생성자에서 말단과 상사를 구분하도록 만들었다.
  • 말단이라면 남아있는 일에 대한 Queue를 하나만 만들어준다.
  • 누군가의 상사라면 남아있는 일에 대한 Queue를 두 개(왼쪽, 오른쪽) 만들어준다.

3. R(날짜) 만큼 반복을 진행하면서 로직을 수행한다.

  • 직원 한 명을 뽑는다. (왼쪽 제일 아래 있는 직원 부터 탐색)
  • 뽑은 직원의 상사 직원을 가져온다.
  • 상사 직원 객체에서 오늘의 날짜(R)에 맞는 남아있는 일에 대한 Queue를 가져와서 부하 직원의 남아있는 일에 대한 Queue에서 하나 뽑아서(poll) 상사 직원 객체의 Queue에 offer() 해준다.
  • 위와 같은 로직을 반복하여 직원 번호가 0인 직원한테 처리된 업무 번호들을 answer에 누적하여 더해준다.
    • 직원 번호가 0인 직원은 가장 위에 있는 대장 직원이기 떄문에 문제에서 요구하는 것이 제일 위에 있는 대장 직원에 의해 처리된 업무 번호들을 더해서 출력하는 문제이다.

 

제출 답안

import java.util.*;
import java.io.*;


public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        String[] HKR = bufferedReader.readLine().split(" ");
        int H = Integer.parseInt(HKR[0]);
        int K = Integer.parseInt(HKR[1]);
        int R = Integer.parseInt(HKR[2]);
        int answer = 0;

        List<Employee> list = new ArrayList<>();
        List<Integer> index = new ArrayList<>();

        for (int i = (int)Math.pow(2, H) - 1; i < (int)Math.pow(2, H + 1) - 1; i++) {
            Employee employee = new Employee(i, false);
            for (String str : bufferedReader.readLine().split(" ")) {
                employee.remainTask.get(0).offer(Integer.valueOf(str));
            }
            list.add(employee);
            index.add(i);
        }

        for (int i = 1; i < R + 1; i++) {
            int size = list.size();
            for (int j = 0; j < size; j++) {
                Employee employee = list.get(j);

                if (employee.getRemainTask(i).isEmpty()) {
                    continue;
                }

                if (employee.employeeNumber == 0) {
                    answer += employee.getRemainTask(i).poll();
                    continue;
                }

                int superiorNumber = employee.getSuperiorNumber();
                Employee superior;

                if (index.contains(superiorNumber)) {
                    superior = list.get(index.indexOf(superiorNumber));
                } else {
                    superior = new Employee(superiorNumber, true);
                    list.add(superior);
                    index.add(superior.employeeNumber);
                }

                if (employee.employeeNumber % 2 == 0) {
                    superior.remainTask.get(1).offer(employee.getRemainTask(i).poll());
                } else {
                    superior.remainTask.get(0).offer(employee.getRemainTask(i).poll());
                }
            }
        }

        System.out.println(answer);
    }
}

class Employee {
    int employeeNumber;
    boolean isSuperior;
    List<Queue<Integer>> remainTask;

    public Employee(int employeeNumber, boolean isSuperior) {
        this.employeeNumber = employeeNumber;
        this.isSuperior = isSuperior;

        remainTask = new ArrayList<>();
        if (isSuperior) {
            remainTask.add(new LinkedList<>()); // 왼쪽 직원이 올린 일
            remainTask.add(new LinkedList<>()); // 오른쪽 직원이 올린 일
        }
        if (!isSuperior) {
            remainTask.add(new LinkedList<>()); //
        }
    }

    public int getSuperiorNumber() {
        if (employeeNumber % 2 != 0) {
            return (employeeNumber - 1) / 2;
        }
        return (employeeNumber - 2) / 2;
    }

    public Queue<Integer> getRemainTask(int date) {
        if (isSuperior) {
            if (date % 2 != 0) {
                return remainTask.get(0);
            }
            return remainTask.get(1);
        }
        return remainTask.get(0);
    }

    public String toString() {
        return String.valueOf(employeeNumber);
    }
}

 

🤔 FeedBack

결과를 보면 정말...집착으로 풀기도 했고, 코드도 가독성이 많이 떨어지는 건 맞다. 그래도 "맞앗습니다!" 이 글자가 보였을 떄...정말...짜릿했다.

 

처음에 소프티어에 홈페이지에 들어가서 Practice를 누르고 제일 위에 있는거 풀어봐야겠다 하고 시작한 것이... 맨날 프로그래머스 레벨 1, 2 풀다가 소프티어 문제 풀고 있는데 안 풀려서 이거 뭐야 하고 있었는데 난이도가 별 3개였다...

 

이미 시작해버렸고...끝은 봐야되기 때문에 계속해서 문제를 풀었다. 믿기지 않겠지만 3시26분까지 계속 하다가 해결이 안돼서 찝찝하게 누웠고, 자기 전에도 계속 생각했다. 

 

일어나서 생각한대로 코드 쭉 작성해보니깐 웬걸 "런타임 에러" 몇 개 빼고는 모두 정답이 나왔다. 오...이거 예외 하나만 잡아주면 통과될 거 같았다. 예상했던대로 나는 Queue를 사용했기 때문에 poll() 메서드를 사용하는데 있어서 예외가 발생해서 런타임 에러가 발생했던 거 같아서 모든 poll() 메서드 앞에 isEmpty()를 사용해서 예외처리를 해줬더니..통과됐다. 😂

 

우아한 테크코스 최종 코딩테스트 후기

 

4주간의 프리코스를 마치고 결과를 기다리던 중 12월 14일에 프리코스 합격 메일을 받았다. 4주 동안의 프리코스도 많은 배움이 있었고, 나름 학업보다 우선순위를 더 높게 두고 매주 과제에 임했다.

 

그래서 합격 메일이 더 기쁘게 다가왔다고 생각했다. 어느 교육보다도 우테코에 꼭 입과하고 싶은 마음이 있었기 때문에...

8호선 잠실역에서 9번 출구에서 내리면 조금 직진하다가 오른쪽으로 꺾으면 바로 큰 건물이 하나 있다. 처음에는 어리둥절하면서 어디지 했었는데 들어가서 층별 안내표를 보고 여기구나라고 생각했다.

 


실제로 와서 우아한형제들이라고 써져 있는 걸 보니깐 더 가고 싶어졌다. 😭

 

7층으로 엘리베이터를 타고 올라가서 왼쪽으로 꺾으면 사람들이 줄을 서있었다. 그대로 쭉 들어가서 주시는 볼펜이랑 노트를 챙기고 물도 챙겨서 자리에 앉으면 된다. 다과도 있었지만 아침부터 배가 너무 아팠어서... 아무것도 먹고 싶다는 생각이 안 들어서 그냥 물만 챙겨서 자리에 앉았다.

 

주시는 볼펜 박스를 열어보고 나서 긴장이 많이 풀렸던 거 같다. 볼펜이 귀여워서...ㅎ

 

 

실제로 대면으로 보는 코딩테스트는 처음이어서 많이 긴장하고 갔었는데 생각보다 되게 자유로운 분위기에서 코딩 테스트를 진행했다. 시험은 5시간 동안 진행되는데 중간에 다과를 먹고 싶으면 먹어도 되고, 담배, 화장실 등 자유롭게 이동해도 된다고 하셨다.

 

코딩테스트가 시작되고 예상은 했지만 정말 한두 시간(?) 동안은 아무도 움직이지 않았던 거 같다. 진짜...열정적인 사람들...정말 우테코에 합격해서 가고 싶었다 😢

 

코딩테스트 문제는 4주간의 프리코스를 자신의 힘으로 해결했었다면 풀 수 있을 정도의 문제가 나왔었다. 처음에 시작할 때 말씀하셨던 것이 "먼저 돌아가는 쓰레기를 만들어라. 리펙토링은 그다음이다."라는 말씀을 하셨는데, 제발 돌아가는 쓰레기라도 만들었으면 하는 마음으로 코딩테스트에 임했다.

 

4시간 정도 흘렀을 때 문제에서 제공하는 테스트 코드가 통과되는 것을 확인했고, 가독성을 높이는 작업 또는 주어진 제한조건을 갖추기 위해서 리펙토링을 진행했다. 최종 제출은 30분 전부터 가능했고 제출을 모두 완료하고 코딩테스트에 대한 소감을 남겼다.

 


모든 제출이 끝나고 시간이 15분 정도 남아서 프로그램을 직접 돌려서 한번 확인해 볼까라는 생각을 하고 돌려봤다. 근데....이게...😭.. 분명 잡았던 예외 케이스였는데 왜...이게 안될까... 뭐지라는 걸 10분 남았을 때 발견했다. 괜히 잘못 건들어서 0점짜리가 될까 봐 건들지 못하고 나왔다.

가독성 작업 전에 테스트 코드에 힘을 좀 써봤으면 어땠을까라는 생각이 들면서 테스트가 끝나고 나왔다. 모르고 나왔으면 좋았을 텐데 마지막에 알고 나와서 하루종일 찝찝했다. 그래도 이렇게 좋은 경험할 수 있게 해 준 우테코에 감사하다고 전하고 싶다.

 

 

 

최종 코딩테스트 결과 발표

오늘이 기다리던 최종 코딩테스트 결과 발표를 하는 날이었다. 백엔드가 이번에 지원자가 엄청 많았다고 했던 거 같다. 2000명..?이었나 어쨌든 많은 사람들 중에 200명 조금 넘는 사람을 뽑았고, 최종에서 100명 정도 뽑는 것으로 알고 있다. 200명 안에 들었다는 것도 놀라운데 100명 안에 들면 얼마나 좋을까라는 생각을 하면서 15시를 기다렸다. 우테코에게 15시는 의미 있는 시간일지도 모른다. 우테코하면서 가장 많이 기다렸던 시간이 15시이다. 그렇게 결과는...

 

 


하하하 😭 예상했지만 더 아팠던 결과였던 거 같다. 마지막 두 문장이 크게 와닿았다. "이번의 불합격으로 인해 프로그래머로 성장하는데 좌절하거나 포기하지 말고 지속해서 도전해 나갔으면 합니다. 그것이 여러분을 불합격시킨 우아한테크코스에 복수할 수 있는 가장 좋은 방법입니다."

 

합격하신 분들 떨어지신 분들 모두 좋은 인연으로 다시 만났으면 좋겠습니다. 다들 정말 열정 넘치시는 분들 이어서 에너지 받을 수 있었습니다. 내년에 6기에 또 지원할지 일을 하고 있을지는 모르겠지만 좋은 배움의 기회였다고 생각합니다.

 

+ Recent posts