현대 그룹에서 만든 Soofteer에서 최근 알고리즘 문제를 풀어봤다. 프로그래머스를 주로 풀면서 느꼈던 차이점은 프로그래머스에서는 입력 값을 직접 받지는 않는 반면에 소프티어에서는 직접 입력 값을 받아서 출력하는식으로 문제를 해결해야 한다.

 

그래서 뭐 하나만 계속 하는것보다는 프로그래머스와 소프티어 둘 다 활용해보면서 입력, 출력 문제도 연습하고, 메서드 파라미터를 활용하여 리턴 값을 가져오는 연습도 해보는 것이 좋다고 느꼈다.

 

평소에 코드 작성 연습을 IntelliJ IDEA에서 했었는데 자동 완성 기능에 익숙해져서 그런지 소프티어 IDE에서만 코드를 작업하는 것이 조금 힘들게 느껴진 부분도 있었다. 예를 들어 IntelliJ에서는 sout 하고 엔터를 치면 System.out.println이 바로 쳐지지만 소프티어 IDE에서는 그렇지 않았다. IDEA에 의존하지 않고 코드를 작성하는 연습이 필요할 것 같다.

 

소프티어에서 Level 2에 해당하는 문제들인 회의실 예약, 전광판, 비밀 메뉴, GBC, 지도 자동 구축, 장애물 인식 프로그램, 8단 변속기, 바이러스, 금고털이를 풀어보면서 느꼈던 점은 같은 Level 2여도... 엄청난 차이가 있는 거 같다.

 

GBC, 회의실 예약 같은 문제는 시간이 생각보다 정말 많이 걸렸다. 출력하는 방법에 미숙하기도 했고, 반복문이 많아지고, 어떤 자료구조를 사용해야 효율적인지 잘 모르겠던 부분이 많았다. 하지만 소프티어의 경우 메모리 제한은 생각보다 넉넉하게 제한하고 있는 것 같아서 메모리가 초과되는 경우는 거의 없었다. 

 

사실 리펙토링도 많이 중요하지만...일단 실행이 되는 것이 우선이기 때문에 코드가 작동되는 것에 먼저 관점을 두고 공부를 해야할 것 같다. Java는 정말...활용도가 엄청난 거 같다. 대신 아는 만큼 더 효율적으로 사용할 수 있다는 것...

 

금고털이

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


public class Main
{
    public static void main(String args[]) throws IOException
    {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        String[] firstInputData = reader.readLine().split(" ");
        int W = Integer.parseInt(firstInputData[0]);
        int N = Integer.parseInt(firstInputData[1]);

        List<Stone> list = new ArrayList();

        for (int i = 0; i < N; i++) {
            String[] stoneData = reader.readLine().split(" ");
            list.add(new Stone(Integer.parseInt(stoneData[0]), Integer.parseInt(stoneData[1])));
        }

        Collections.sort(list);

        int result = 0;
        int stoneIndex = 0;
        while (true) {
            Stone stone = list.get(stoneIndex);
            if (stone.M < W) {
                W -= stone.M;
                result += stone.M * stone.P;
                stoneIndex++;
                continue;
            }
            result += W * stone.P;
            break;
        }

        System.out.println(result);
    }
}

class Stone implements Comparable<Stone> {
    int M;
    int P;

    public Stone(int M, int P) {
        this.M = M;
        this.P = P;
    }

    @Override
    public int compareTo(Stone obj) {
        return obj.P - this.P;
    }
}

 

바이러스

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


public class Main
{
    public static void main(String args[]) throws IOException
    {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer tokenizer = new StringTokenizer(reader.readLine());

        long K = Integer.parseInt(tokenizer.nextToken());
        long P = Integer.parseInt(tokenizer.nextToken());
        int N = Integer.parseInt(tokenizer.nextToken());

        for (int i = 1; i <= N; i++) {
            K = K * P % 1000000007;
        }

        System.out.println(K);
    }
}

 

8단 변속기

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


public class Main
{
    public static void main(String args[]) throws IOException
    {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        String inputData = reader.readLine();

        if (inputData.equals("1 2 3 4 5 6 7 8")) {
            System.out.println("ascending");
        } else if (inputData.equals("8 7 6 5 4 3 2 1")) {
            System.out.println("descending");
        } else {
            System.out.println("mixed");
        }

        
    }
}

 

장애물 인식 프로그램

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


public class Main
{
    static int count = 0;

    public static void main(String args[]) throws IOException
    {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int N = Integer.parseInt(br.readLine());
        int[][] graph = new int[N][N];
        List<Integer> result = new ArrayList<>();

        for (int i = 0; i < N; i++) {
            String[] data = br.readLine().split("");
            for (int j = 0; j < N; j++) {
                graph[i][j] = Integer.parseInt(data[j]);
            }
        }

        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                count = 0;
                if (graph[i][j] == 0) {
                    continue;
                }
                dfs(i, j, graph, N);
                result.add(count);
            }
        }

        Collections.sort(result);
        System.out.println(result.size());
        result.stream().forEach(System.out::println);
    }

    public static void dfs(int i, int j, int[][] graph, int N) {
        if (i < 0 || j < 0 || i >= N || j >= N) {
            return;
        }

        if (graph[i][j] == 0) {
            return;
        }

        if (graph[i][j] == 1) {
            graph[i][j] = 0;
            count++;
            dfs(i, j + 1, graph, N);
            dfs(i, j - 1, graph, N);
            dfs(i + 1, j, graph, N);
            dfs(i - 1, j, graph, N);
        }
    }
}

 

지도 자동 구축

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


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

        int N = Integer.parseInt(reader.readLine());

        int result = getResult(N);

        System.out.println(result * result);
    }

    public static int getResult(int N) {
        if (N == 0) {
            return 2;
        }
        return getResult(N - 1) + (int)Math.pow(2, N - 1);
    }
}

 

GBC

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


public class Main
{
    public static void main(String args[]) throws IOException
    {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        
        StringTokenizer tokenizer = new StringTokenizer(reader.readLine());
        int n = Integer.parseInt(tokenizer.nextToken());
        int m = Integer.parseInt(tokenizer.nextToken());

        Section nSection = new Section();
        int before = 0;

        for (int i = 0; i < n; i++) {
            tokenizer = new StringTokenizer(reader.readLine());
            nSection.scope.add(before + Integer.parseInt(tokenizer.nextToken()));
            nSection.speed.add(Integer.parseInt(tokenizer.nextToken()));

            before = nSection.scope.get(i);
        }

        Section mSection = new Section();
        before = 0;
        
        for (int i = 0; i < m; i++) {
            tokenizer = new StringTokenizer(reader.readLine());
            mSection.scope.add(before + Integer.parseInt(tokenizer.nextToken()));
            mSection.speed.add(Integer.parseInt(tokenizer.nextToken()));

            before = mSection.scope.get(i);
        }

        int result = 0;
        before = 0;

        for (int i = 0; i < n; i++) {
            int sectionLimitSpeed = nSection.speed.get(i);
            int sectionMaxSpeed = mSection.getMaxSpeed(before, nSection.scope.get(i));
            result = Math.max(result, sectionMaxSpeed - sectionLimitSpeed);
            before = nSection.scope.get(i) + 1;
        }

        System.out.println(result);
    }
}

class Section {
    List<Integer> scope = new ArrayList<>();
    List<Integer> speed = new ArrayList<>();

    public int getMaxSpeed(int start, int end) {
        int s = 0;

        for (int i = start; i <= end; i++) {
            s = Math.max(s, getSpeed(i));
        }

        return s;
    }

    public int getSpeed(int value) {
        int s = 0;

        for (int i = 0; i < scope.size(); i++) {
            if (value <= scope.get(i)) {
                s = speed.get(i);
                break;
            }
        }

        return s;
    }
}

 

[21년 재직자 대회 예선] 비밀 메뉴

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


public class Main
{
    public static void main(String args[]) throws IOException
    {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        
        String[] inputData = reader.readLine().split(" ");
        int M = Integer.parseInt(inputData[0]);
        int N = Integer.parseInt(inputData[1]);
        int K = Integer.parseInt(inputData[2]);

        String[] SecretCode = reader.readLine().split(" ");
        String[] MyCode = reader.readLine().split(" ");

        boolean isSecret = false;

        for (int i = 0; i < MyCode.length; i++) {
            if (MyCode.length - i < SecretCode.length || isSecret) {
                break;
            }

            if (MyCode[i].equals(SecretCode[0])) {
                isSecret = Arrays.equals(Arrays.copyOfRange(MyCode, i, i + SecretCode.length), SecretCode);
            }
        }

        if (isSecret) {
            System.out.println("secret");
        } else {
            System.out.println("normal");
        }
    }
}

 

[21년 재직자 대회 예선] 전광판

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


public class Main
{
    private static final List<int[]> digitInfo = new ArrayList<>() {{
        add(new int[]{1, 1, 1, 0, 1, 1, 1});
        add(new int[]{0, 0, 1, 0, 0, 1, 0});
        add(new int[]{1, 0, 1, 1, 1, 0, 1});
        add(new int[]{1, 0, 1, 1, 0, 1, 1});
        add(new int[]{0, 1, 1, 1, 0, 1, 0});
        add(new int[]{1, 1, 0, 1, 0, 1, 1});
        add(new int[]{1, 1, 0, 1, 1, 1, 1});
        add(new int[]{1, 1, 1, 0, 0, 1, 0});
        add(new int[]{1, 1, 1, 1, 1, 1, 1});
        add(new int[]{1, 1, 1, 1, 0, 1, 1});
    }};

    public static void main(String args[]) throws IOException
    {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        int T = Integer.parseInt(reader.readLine());

        for (int i = 0; i < T; i++) {
            StringTokenizer tokenizer = new StringTokenizer(reader.readLine());
            int firstValue = Integer.parseInt(tokenizer.nextToken());
            int secondValue = Integer.parseInt(tokenizer.nextToken());

            System.out.println(testCase(firstValue, secondValue));
        }
    }

    public static int testCase(int firstValue, int secondValue) {
        int result = 0;

        List<Integer> firstValueDigit = getDigitInfo(firstValue);
        List<Integer> secondValueDigit = getDigitInfo(secondValue);

        for (int i = 0; i < 5; i++) {
            result += getChangeCount(firstValueDigit.get(i), secondValueDigit.get(i));
        }

        return result;
    }

    public static int getChangeCount(int x, int y) {
        if (x == -1 && y == -1) {
            return 0;
        }

        if (x == -1) {
            return (int)Arrays.stream(digitInfo.get(y)).filter(data -> data == 1).count();
        }

        if (y == -1) {
            return (int)Arrays.stream(digitInfo.get(x)).filter(data -> data == 1).count();
        }

        int changeCount = 0;
        int[] xInfo = digitInfo.get(x);
        int[] yInfo = digitInfo.get(y);

        for (int i = 0; i < 7; i++) {
            if (xInfo[i] != yInfo[i]) {
                changeCount++;
            }
        }

        return changeCount;
    }

    public static List<Integer> getDigitInfo(int value) {
        List<Integer> info = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            info.add(value / 10000);
            info.add(value % 10000 / 1000);
            info.add(value % 1000 / 100);
            info.add(value % 100 / 10);
            info.add(value % 10);
        }

        int length = String.valueOf(value).length();

        for (int i = 0; i < 5 - length; i++) {
            info.set(i, -1);
        }

        return info;
    }
}

 

[21년 재직자 대회 예선] 회의실 예약

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


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

        String[] firstInputData = reader.readLine().split(" ");

        int N = Integer.parseInt(firstInputData[0]);
        int M = Integer.parseInt(firstInputData[1]);

        Map<String, ConferenceRoom> conferenceRoom = new HashMap<String, ConferenceRoom>();

        for (int i = 0; i < N; i++) {
            String name = reader.readLine();
            conferenceRoom.put(name, new ConferenceRoom(name));
        }

        for (int i = 0; i < M; i++) {
            String[] conferenceData = reader.readLine().split(" ");
            String name = conferenceData[0];
            int start = Integer.parseInt(conferenceData[1]);
            int end = Integer.parseInt(conferenceData[2]);

            List<Boolean> currentAvailable = conferenceRoom.get(name).available;
            
            for (int j = start - 9; j <= end - 10; j++) {
                currentAvailable.set(j, false);
            }
            
        }

        List<String> sortedKey = new ArrayList<>(conferenceRoom.keySet());
        Collections.sort(sortedKey);

        for (int i = 0; i < sortedKey.size(); i++) {
            System.out.println("Room " + conferenceRoom.get(sortedKey.get(i)).name + ":");

            ConferenceRoom pickedConferenceRoom = conferenceRoom.get(sortedKey.get(i));
            List<Boolean> currentAvailable = pickedConferenceRoom.available;

            if (!currentAvailable.contains(true)) {
                System.out.println("Not available");
            } else {
                System.out.println(pickedConferenceRoom.getCount() + " available:");
                pickedConferenceRoom.printCurrentAvailable();
            }
            
            if (i != sortedKey.size() - 1) {
                System.out.println("-----");
            }
        }
    }
}

class ConferenceRoom {
    String name;
    List<Boolean> available;
    List<int[]> availableTime;

    public ConferenceRoom(String name) {
        this.name = name;

        available = new ArrayList<Boolean>();

        for (int i = 0; i < 9; i++) {
            available.add(true);
        }

        availableTime = new ArrayList<int[]>();
    }

    public int getCount() {
        int count = 0;
        boolean before = false;

        int start = 0;
        int end = 0;

        for (int i = 0; i < available.size(); i++) {
            if (!before && available.get(i)) {
                start = i + 9;
                before = true;
            } else if (before && !available.get(i)) {
                end = i + 9;
                before = false;
                count++;
                availableTime.add(new int[]{start, end});
            }
        }

        if (before) {
            count++;
            availableTime.add(new int[]{start, 18});
        }

        return count;
    }

    public void printCurrentAvailable() {
        for (int[] a : availableTime) {
            System.out.printf("%02d-%02d\n", a[0], a[1]);
        }
    }
}

 

정답을 맞추기 위해서 풀었던 문제들이 많았기에...코드가 가독성이 많이 떨어집니다... 그냥 기록이라도 해놓기 위해서 올려둔 것이기 때문에 불편해도 어쩔 수 없습니다. 하하하 🤓

 

 

 

알고리즘 문제를 풀면서 가장 많이 사용되었던 것이 무엇이냐고 물어보면 문자열을 쪼개서 활용하는 문제들이 많았다고 말할 수 있을 거 같다. 상황에 따라서 선택할 수 있도록 String의 split()과 StringTokenizer을 사용하여 문자열을 자르는 예제를 작성해보려고 한다.

 

일단 split()과 StringTokenizer 둘다 문자열을 특정 구분자(delimiter)을 기준으로 쪼갤 수 있는 기능을 가지고 있다. 사실 이번 글을 정리하게된 계기도 평소에 문자열을 자를 때는 항상 split()을 사용했다. 하지만, 최근데 입력을 받을 때 사용하는 BufferedReader를 사용하며 StringTokenizer에도 관심을 갖게 되었다.

 

첫 번째로 살펴볼 예제는 "가,나,다,라,마,바,사"를 구분자(,)를 기준으로 잘라서 "가 나 다 라 마 바 사"를 출력하게 하는 예제를 살펴볼 것이다.

 

split()을 이용하기

public class splitEx {
    public static void main(String[] args) {
        String str = "가,나,다,라,마,바,사";

        String[] splitStr = str.split(",");

        for (String s : splitStr) {
            System.out.print(s + " ");
        }
    }
}
출력
가 나 다 라 마 바 사 

split()의 경우에는 String 클래스의 메서드이기 때문에 별도로 import 해줘야 하는 것은 없다. 간단하게 나눌 수 있다.

 

StringTokenizer을 이용하기

import java.util.StringTokenizer;

public class StringTokenizerEx {
    public static void main(String[] args) {
        String str = "가,나,다,라,마,바,사";

        StringTokenizer tokenizer = new StringTokenizer(str, ",");

        while (tokenizer.hasMoreTokens()) {
            System.out.print(tokenizer.nextToken() + " ");
        }
    }
}
출력
가 나 다 라 마 바 사 

split()과는 상당히 유사한 형태를 띄고있긴 하다. StringTokenizer의 경우 java.util 패키지 안에 있기 때문에 import를 해주고 사용해야 한다. 

 

StringTokenizer의 생성자에 대해서 한번 알아보자.

 

위 코드에서는 두 번째 파라미터가 2개인 생성자를 사용해서 만든 StringTokenizer입니다. 설정 구분자인 ","를 기준으로 문자를 나눌 수 있었습니다. 이번에는 StringTokenizer의 메서드를 한번 살펴보겠습니다.

 

int countTokens()

현재 남아있는 Token의 개수를 반환합니다. 전체 Token 개수가 아니고 남은 Token의 개수입니다.

 

boolean hasMoreTokens()

StringTokenizer 객체에서 읽어올 수 있는 다음 토큰이 존재하면 true를 반환합니다.

 

String nextToken()

StringTokenizer 객체에서 다음 토큰을 갖와서 반환합니다. 이때 반환되는 타입은 String 타입입니다.

 

위에 3개가 StringTokenizer을 사용하여 문자열을 나눌 때, 자주 사용되는 메서드입니다. 별로 많지 않아서 쉽게 익히고 사용할 수 있습니다. 

 

 

이번에는 split()을 유용하게 사용할 수 있는 한 가지 예를 하나 사용해보고 마무리를 하겠습니다.

 

"가a나b다c라d마e바f사g"라는 문자열이 있을 때, "가 나 다 라 마 바 사"의 출력을 얻고 싶다면 어떻게 해야할까요.

 

문자열을 charAt()을 통해서 하나 하나 확인하면서 a, b, c, d, e, f, g에 해당하는 문자열을 빼고, 나머지만 찾아내는 방법을 써도 해결을 할 수 있을 거 같습니다. 이때 split()의 기능을 사용하면 간단하게 문자열을 구분할 수 있습니다.

 

구분자가 a, b, c, d, e, f, g 여러개인데 어떻게 나누냐는 생각을 할 수도 있습니다. 이때 split()에는 "|" 를 가지고 구분하면 여러 가지 구분자를 같이 사용할 수 있습니다. 코드로 한번 살펴보겠습니다.

 

여러가지 구분자를 사용하는 split()

package practice;

public class splitEx {
    public static void main(String[] args) {
        String str = "가a나b다c라d마e바f사g";

        String[] splitStr = str.split("a|b|c|d|e|f|g");

        for (String s : splitStr) {
            System.out.print(s + " ");
        }
    }
}
출력
가 나 다 라 마 바 사 

split()을 사용해서 "|"로 구분해서 여러가지 구분자를 사용하면 위와 같은 결과를 쉽게 얻을 수 있습니다. 아는 만큼 사용할 수 있는 것이 많아지기 때문에 많은 것을 일단 보고, 사용해보는 것이 좋다고 생각합니다.

학교 수업에서 데이터를 입력받을 때, 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 스크립트가 서버를 돌렸을 때, 데이터가 초기화되는 것을 확인할 수 있습니다.

+ Recent posts