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

 

프로그래머스

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

programmers.co.kr


문제 설명

오픈채팅방

카카오톡 오픈채팅방에서는 친구가 아닌 사람들과 대화를 할 수 있는데, 본래 닉네임이 아닌 가상의 닉네임을 사용하여 채팅방에 들어갈 수 있다.

신입사원인 김크루는 카카오톡 오픈 채팅방을 개설한 사람을 위해, 다양한 사람들이 들어오고, 나가는 것을 지켜볼 수 있는 관리자창을 만들기로 했다. 채팅방에 누군가 들어오면 다음 메시지가 출력된다.

"[닉네임]님이 들어왔습니다."

채팅방에서 누군가 나가면 다음 메시지가 출력된다.

"[닉네임]님이 나갔습니다."

채팅방에서 닉네임을 변경하는 방법은 다음과 같이 두 가지이다.

  • 채팅방을 나간 후, 새로운 닉네임으로 다시 들어간다.
  • 채팅방에서 닉네임을 변경한다.

닉네임을 변경할 때는 기존에 채팅방에 출력되어 있던 메시지의 닉네임도 전부 변경된다.

예를 들어, 채팅방에 "Muzi"와 "Prodo"라는 닉네임을 사용하는 사람이 순서대로 들어오면 채팅방에는 다음과 같이 메시지가 출력된다.

"Muzi님이 들어왔습니다."
"Prodo님이 들어왔습니다."

채팅방에 있던 사람이 나가면 채팅방에는 다음과 같이 메시지가 남는다.

"Muzi님이 들어왔습니다."
"Prodo님이 들어왔습니다."
"Muzi님이 나갔습니다."

Muzi가 나간후 다시 들어올 때, Prodo 라는 닉네임으로 들어올 경우 기존에 채팅방에 남아있던 Muzi도 Prodo로 다음과 같이 변경된다.

"Prodo님이 들어왔습니다."
"Prodo님이 들어왔습니다."
"Prodo님이 나갔습니다."
"Prodo님이 들어왔습니다."

채팅방은 중복 닉네임을 허용하기 때문에, 현재 채팅방에는 Prodo라는 닉네임을 사용하는 사람이 두 명이 있다. 이제, 채팅방에 두 번째로 들어왔던 Prodo가 Ryan으로 닉네임을 변경하면 채팅방 메시지는 다음과 같이 변경된다.

"Prodo님이 들어왔습니다."
"Ryan님이 들어왔습니다."
"Prodo님이 나갔습니다."
"Prodo님이 들어왔습니다."

채팅방에 들어오고 나가거나, 닉네임을 변경한 기록이 담긴 문자열 배열 record가 매개변수로 주어질 때, 모든 기록이 처리된 후, 최종적으로 방을 개설한 사람이 보게 되는 메시지를 문자열 배열 형태로 return 하도록 solution 함수를 완성하라.

제한사항
  • record는 다음과 같은 문자열이 담긴 배열이며, 길이는 1 이상 100,000 이하이다.
  • 다음은 record에 담긴 문자열에 대한 설명이다.
    • 모든 유저는 [유저 아이디]로 구분한다.
    • [유저 아이디] 사용자가 [닉네임]으로 채팅방에 입장 - "Enter [유저 아이디] [닉네임]" (ex. "Enter uid1234 Muzi")
    • [유저 아이디] 사용자가 채팅방에서 퇴장 - "Leave [유저 아이디]" (ex. "Leave uid1234")
    • [유저 아이디] 사용자가 닉네임을 [닉네임]으로 변경 - "Change [유저 아이디] [닉네임]" (ex. "Change uid1234 Muzi")
    • 첫 단어는 Enter, Leave, Change 중 하나이다.
    • 각 단어는 공백으로 구분되어 있으며, 알파벳 대문자, 소문자, 숫자로만 이루어져있다.
    • 유저 아이디와 닉네임은 알파벳 대문자, 소문자를 구별한다.
    • 유저 아이디와 닉네임의 길이는 1 이상 10 이하이다.
    • 채팅방에서 나간 유저가 닉네임을 변경하는 등 잘못 된 입력은 주어지지 않는다.
입출력 예
record result
["Enter uid1234 Muzi", "Enter uid4567 Prodo","Leave uid1234","Enter uid1234 Prodo","Change uid4567 Ryan"] ["Prodo님이 들어왔습니다.", "Ryan님이 들어왔습니다.", "Prodo님이 나갔습니다.", "Prodo님이 들어왔습니다."]
입출력 예 설명

입출력 예 #1
문제의 설명과 같다.


문제 포인트

1. 'Enter' 인 경우 'id + 출력 메세지' al에 String 형태로 추가
2. 'Leave' 인 경우 '출력메세지'만 al에 String 형태로 추가
3. 'Change'인 경우 기존 map 저장되있는 id를 변경된 id로 바꿔서 map에 저장

마지막에 'id (key) + 출력메세지'로 저장되있는 al를 '님' 문자열 앞부분 기준으로 잘라서 map에서 get!
나온 닉네임(value)으로 '닉네임 + 출력메세지'로 출력


 

import java.util.*;

class Solution {
    public String[] solution(String[] record) {


        HashMap<String, String> map = new HashMap<>();
        ArrayList<String> al = new ArrayList<>();

        // 각 명령어 대로 key+"님이 ~~~" 형식으로 al에 저장
        for (String str : record) {

            String command[] = str.split(" ");

            switch (command[0]) {

                // 들어온경우 Map 에 Key를 ID , value를 닉네임으로 저장 (uid1234 , Muzi) 
                // 들어온 id + 메세지 al에 추가
                case "Enter":
                    map.put(command[1], command[2]);
                    al.add(command[1] + "님이 들어왔습니다.");
                    break;
                // 나가는 경우 나가는 아이디 + 메세지만 al에 추가
                case "Leave":
                    al.add(command[1] + "님이 나갔습니다.");
                    break;
                // 해당  key 값에 대한 value 만 변경
                case "Change":
                    map.replace(command[1], command[2]);
                    break;
            }
        }

        // record 의 크기로 선언해주면 change 명령어까지 카운트 되므로 al.size로 선언 (메세지 출력수)  
        String[] answer = new String[al.size()];

        for (int i = 0; i < al.size(); i++) {
            String tmp = al.get(i);
            int idx = tmp.indexOf('님'); // '님'이 나오는 인덱스를 기억해서
            String key_id = tmp.substring(0, idx); // '님'이 나오기 직전 idx 까지 key_id 에 저장
            answer[i] = tmp.replace(key_id, map.get(key_id)); // key_id를 value 로 replace 로 하여 answer[i] 에 저장
        }

        return answer;
    }
}

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

 

프로그래머스

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

programmers.co.kr

문제 설명

주차장의 요금표와 차량이 들어오고(입차) 나간(출차) 기록이 주어졌을 때, 차량별로 주차 요금을 계산하려고 합니다. 아래는 하나의 예시를 나타냅니다.

  • 요금표

기본 시간(분)기본 요금(원)단위 시간(분)단위 요금(원)

180 5000 10 600

 

  • 입/출차 기록

시각(시:분)차량 번호내역

05:34 5961 입차
06:00 0000 입차
06:34 0000 출차
07:59 5961 출차
07:59 0148 입차
18:59 0000 입차
19:09 0148 출차
22:59 5961 입차
23:00 5961 출차

 

  • 자동차별 주차 요금

차량 번호누적 주차 시간(분)주차 요금(원)

0000 34 + 300 = 334 5000 + ⌈(334 - 180) / 10⌉ x 600 = 14600
0148 670 5000 +⌈(670 - 180) / 10⌉x 600 = 34400
5961 145 + 1 = 146 5000
  • 어떤 차량이 입차된 후에 출차된 내역이 없다면, 23:59에 출차된 것으로 간주합니다.
    • 0000번 차량은 18:59에 입차된 이후, 출차된 내역이 없습니다. 따라서, 23:59에 출차된 것으로 간주합니다.
  • 00:00부터 23:59까지의 입/출차 내역을 바탕으로 차량별 누적 주차 시간을 계산하여 요금을 일괄로 정산합니다.
  • 누적 주차 시간이 기본 시간이하라면, 기본 요금을 청구합니다.
  • 누적 주차 시간이 기본 시간을 초과하면, 기본 요금에 더해서, 초과한 시간에 대해서 단위 시간 마다 단위 요금을 청구합니다.
    • 초과한 시간이 단위 시간으로 나누어 떨어지지 않으면, 올림합니다.
    • ⌈a⌉ : a보다 작지 않은 최소의 정수를 의미합니다. 즉, 올림을 의미합니다.

주차 요금을 나타내는 정수 배열 fees, 자동차의 입/출차 내역을 나타내는 문자열 배열 records가 매개변수로 주어집니다. 차량 번호가 작은 자동차부터 청구할 주차 요금을 차례대로 정수 배열에 담아서 return 하도록 solution 함수를 완성해주세요.

제한사항

  • fees의 길이 = 4
    • fees[0] = 기본 시간(분)
    • 1 ≤ fees[0] ≤ 1,439
    • fees[1] = 기본 요금(원)
    • 0 ≤ fees[1] ≤ 100,000
    • fees[2] = 단위 시간(분)
    • 1 ≤ fees[2] ≤ 1,439
    • fees[3] = 단위 요금(원)
    • 1 ≤ fees[3] ≤ 10,000
  • 1 ≤ records의 길이 ≤ 1,000
    • records의 각 원소는 "시각 차량번호 내역" 형식의 문자열입니다.
    • 시각, 차량번호, 내역은 하나의 공백으로 구분되어 있습니다.
    • 시각은 차량이 입차되거나 출차된 시각을 나타내며, HH:MM 형식의 길이 5인 문자열입니다.
      • HH:MM은 00:00부터 23:59까지 주어집니다.
      • 잘못된 시각("25:22", "09:65" 등)은 입력으로 주어지지 않습니다.
    • 차량번호는 자동차를 구분하기 위한, `0'~'9'로 구성된 길이 4인 문자열입니다.
    • 내역은 길이 2 또는 3인 문자열로, IN 또는 OUT입니다. IN은 입차를, OUT은 출차를 의미합니다.
    • records의 원소들은 시각을 기준으로 오름차순으로 정렬되어 주어집니다.
    • records는 하루 동안의 입/출차된 기록만 담고 있으며, 입차된 차량이 다음날 출차되는 경우는 입력으로 주어지지 않습니다.
    • 같은 시각에, 같은 차량번호의 내역이 2번 이상 나타내지 않습니다.
    • 마지막 시각(23:59)에 입차되는 경우는 입력으로 주어지지 않습니다.
    • 아래의 예를 포함하여, 잘못된 입력은 주어지지 않습니다.
      • 주차장에 없는 차량이 출차되는 경우
      • 주차장에 이미 있는 차량(차량번호가 같은 차량)이 다시 입차되는 경우

입출력 예

fees records result
[180, 5000, 10, 600] ["05:34 5961 IN", "06:00 0000 IN", "06:34 0000 OUT", "07:59 5961 OUT", "07:59 0148 IN", "18:59 0000 IN", "19:09 0148 OUT", "22:59 5961 IN", "23:00 5961 OUT"] [14600, 34400, 5000]
[120, 0, 60, 591] ["16:00 3961 IN","16:00 0202 IN","18:00 3961 OUT","18:00 0202 OUT","23:58 3961 IN"] [0, 591]
[1, 461, 1, 10] ["00:00 1234 IN"] [14841]

입출력 예 설명

입출력 예 #1

문제 예시와 같습니다.

입출력 예 #2

  • 요금표

기본 시간(분)기본 요금(원)단위 시간(분)단위 요금(원)

120 0 60 591

 

  • 입/출차 기록

시각(시:분)차량 번호내역

16:00 3961 입차
16:00 0202 입차
18:00 3961 출차
18:00 0202 출차
23:58 3961 입차

 

  • 자동차별 주차 요금

차량 번호누적 주차 시간(분)주차 요금(원)

0202 120 0
3961 120 + 1 = 121 0 +[(121 - 120) / 60]x 591 = 591
  • 3961번 차량은 2번째 입차된 후에는 출차된 내역이 없으므로, 23:59에 출차되었다고 간주합니다.

 

입출력 예 #3

  • 요금표

기본 시간(분)기본 요금(원)단위 시간(분)단위 요금(원)

1 461 1 10

 

  • 입/출차 기록

시각(시:분)차량 번호내역

00:00 1234 입차

 

  • 자동차별 주차 요금

차량 번호누적 주차 시간(분)주차 요금(원)

1234 1439 461 +[(1439 - 1) / 1]x 10 = 14841
  • 1234번 차량은 출차 내역이 없으므로, 23:59에 출차되었다고 간주합니다.

 

 


import java.util.*;

class Solution {
    public int[] solution(int[] fees, String[] records) {
        int[] answer = {};
        // 기본 시간(분) / 기본 요금(원) / 단위 시간(분) /단위 요금(원)

        HashMap<String, Integer> map = new HashMap<>();
        HashMap<String, Integer> result_map = new HashMap<>();      // 결과값 저장

        // 주차시간 누적시키기
        for (int i = 0; i < records.length; i++) {
            String car_record[] = records[i].split(" ");

            if (car_record[2].equals("IN")) {
                // 입차일 경우 ( 차량번호(key) , 입차시간(value) ) 형태로 map 에 저장
                map.put(car_record[1], hourtomin(car_record[0]));
            } else if (car_record[2].equals("OUT")) {
                // 출차일 경우
                int parking_time = map.get(car_record[1]);   // 주차시간
                int exiting_time = hourtomin(car_record[0]); // 출차시간

                int cal_time = exiting_time - parking_time; // 계산해야되는 시간(분)

                if (result_map.get(car_record[1]) == null)
                    result_map.put(car_record[1], cal_time);
                else
                    result_map.put(car_record[1], result_map.get(car_record[1]) + cal_time);

                map.remove(car_record[1]);
            }
        }

        // 남은 차 계산
        for (String key : map.keySet()) {
            int time = 0;
            if (result_map.get(key) != null)
                time = result_map.get(key);

            int cal_time = 1439 - map.get(key);

            result_map.put(key, time + cal_time);
        }

        List<String> al = new ArrayList<>(result_map.keySet());

        // 키 값으로 오름차순 정렬
        Collections.sort(al);

        answer = new int[al.size()];
        int idx = 0;

        // 금액계산
        for (String key : al) {

            int time = result_map.get(key); // 해당 차의 주차 총 시간 (분)

            // 기본시간보다 적으면
            if (time <= fees[0]) {
//                result_map.put(key, fees[1]);
                answer[idx] = fees[1];
            }
            // 기본시간보다 많으면
            else {
                double tmp = Math.ceil((time - fees[0]) / (double) fees[2]);
                answer[idx] = fees[1] + (int) tmp * fees[3];
            }
            idx++;
        }

        return answer;
    }

    public int hourtomin(String time) {
        String tmp[] = time.split(":");
        int hour = Integer.parseInt(tmp[0]); // 시간
        int minutes = Integer.parseInt(tmp[1]); // 분

        return hour * 60 + minutes;
    }
}

 

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

 

프로그래머스

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

programmers.co.kr

문제 설명

전화번호부에 적힌 전화번호 중, 한 번호가 다른 번호의 접두어인 경우가 있는지 확인하려 합니다.
전화번호가 다음과 같을 경우, 구조대 전화번호는 영석이의 전화번호의 접두사입니다.

  • 구조대 : 119
  • 박준영 : 97 674 223
  • 지영석 : 11 9552 4421

전화번호부에 적힌 전화번호를 담은 배열 phone_book 이 solution 함수의 매개변수로 주어질 때, 어떤 번호가 다른 번호의 접두어인 경우가 있으면 false를 그렇지 않으면 true를 return 하도록 solution 함수를 작성해주세요.

제한 사항
  • phone_book의 길이는 1 이상 1,000,000 이하입니다.
    • 각 전화번호의 길이는 1 이상 20 이하입니다.
    • 같은 전화번호가 중복해서 들어있지 않습니다.
입출력 예제
phone_book return
["119", "97674223", "1195524421"] false
["123","456","789"] true
["12","123","1235","567","88"] false
입출력 예 설명

입출력 예 #1
앞에서 설명한 예와 같습니다.

입출력 예 #2
한 번호가 다른 번호의 접두사인 경우가 없으므로, 답은 true입니다.

입출력 예 #3
첫 번째 전화번호, “12”가 두 번째 전화번호 “123”의 접두사입니다. 따라서 답은 false입니다.


문제 포인트

처음에 직관적으로 2중 포문을 돌려서 해당 전화번호가 앞에 있으면 접두어인 걸로 구현하였으나 그렇게 하면 효율성 false,,,, 다른 방법 찾아보니 Collections.sort(al); 해서 바로 옆끼리만 체크하면 되는 방법.. 발견.... 천재다... 

※ List<Sring> al = Arrays.asList(phone_book) <- 이것도 기억해두자 Array 를 ArrayList로 변환하는 방법


// 119 , 12119 테스트케이스 추가해보기
import java.util.*;

class Solution {
    public boolean solution(String[] phone_book) {
        boolean answer = true;

        List<String> al = Arrays.asList(phone_book);

        Collections.sort(al);

        for (int i = 0; i < al.size() - 1; i++) {

            // 
            if (al.get(i+1).contains(al.get(i)) && al.get(i+1).charAt(0) == al.get(i).charAt(0)) {
                answer = false;
                break;
            }
//            String cur = al.get(i);
//            String target = al.get(i + 1).substring(0, cur.length());
//            if (cur.equals(target)) {
//                answer = false;
//                break;
//            }
        }

        /* 효율성 테스트 실패
//        String prefix = phone_book[0];
//        int prefix_length = prefix.length();
//        Loop1:
//        for (int i = 0; i < phone_book.length; i++) {
//            String cur = phone_book[i];
//            int cur_length = cur.length();
//
//            for (int j = i + 1; j < phone_book.length; j++) {
//                if (cur_length <= phone_book[j].length()) {
//                    String target = phone_book[j].substring(0, cur_length);
//
//                    if (target.equals(cur)) {
//                        answer = false;
//                        break Loop1;
//                    }
//                }
//            }
//        }

         */
        return answer;
    }
}

 

 

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

 

프로그래머스

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

programmers.co.kr

문제 설명

스파이들은 매일 다른 옷을 조합하여 입어 자신을 위장합니다.

예를 들어 스파이가 가진 옷이 아래와 같고 오늘 스파이가 동그란 안경, 긴 코트, 파란색 티셔츠를 입었다면 다음날은 청바지를 추가로 입거나 동그란 안경 대신 검정 선글라스를 착용하거나 해야 합니다.

종류이름
얼굴 동그란 안경, 검정 선글라스
상의 파란색 티셔츠
하의 청바지
겉옷 긴 코트

스파이가 가진 의상들이 담긴 2차원 배열 clothes가 주어질 때 서로 다른 옷의 조합의 수를 return 하도록 solution 함수를 작성해주세요.

제한사항
  • clothes의 각 행은 [의상의 이름, 의상의 종류]로 이루어져 있습니다.
  • 스파이가 가진 의상의 수는 1개 이상 30개 이하입니다.
  • 같은 이름을 가진 의상은 존재하지 않습니다.
  • clothes의 모든 원소는 문자열로 이루어져 있습니다.
  • 모든 문자열의 길이는 1 이상 20 이하인 자연수이고 알파벳 소문자 또는 '_' 로만 이루어져 있습니다.
  • 스파이는 하루에 최소 한 개의 의상은 입습니다.
입출력 예
clothes return
[["yellow_hat", "headgear"], ["blue_sunglasses", "eyewear"], ["green_turban", "headgear"]] 5
[["crow_mask", "face"], ["blue_sunglasses", "face"], ["smoky_makeup", "face"]] 3
입출력 예 설명

예제 #1
headgear에 해당하는 의상이 yellow_hat, green_turban이고 eyewear에 해당하는 의상이 blue_sunglasses이므로 아래와 같이 5개의 조합이 가능합니다.

1. yellow_hat
2. blue_sunglasses
3. green_turban
4. yellow_hat + blue_sunglasses
5. green_turban + blue_sunglasses

예제 #2
face에 해당하는 의상이 crow_mask, blue_sunglasses, smoky_makeup이므로 아래와 같이 3개의 조합이 가능합니다.

1. crow_mask
2. blue_sunglasses
3. smoky_makeup

문제포인트

문제가 길어 복잡해보이지만 조금만 생각하면 단순하게 풀수있는 문제였다. HaspMap 에 (옷 종류, 해당 옷 분류의 수)를 put하여서 조합되어 나오는 경우의 수를 구하였다. 쉬었던 문제!!


import java.util.*;

class Solution {
    public int solution(String[][] clothes) {
        int answer = 1;

        HashMap<String, Integer> map = new HashMap<>();

        for (int i = 0; i < clothes.length; i++) {
            String key = clothes[i][1];

            if (map.containsKey(key)) map.put(clothes[i][1], map.get(key) + 1);
            else map.put(key, 1);

        }

        for (String str : map.keySet())
            answer *= map.get(str)+1;

        // 아얘 안입는 경우 빼야됨
        return answer-1;
    }
}

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

 

프로그래머스

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

programmers.co.kr

문제 설명

무인도에 갇힌 사람들을 구명보트를 이용하여 구출하려고 합니다. 구명보트는 작아서 한 번에 최대 2명씩 밖에 탈 수 없고, 무게 제한도 있습니다.

예를 들어, 사람들의 몸무게가 [70kg, 50kg, 80kg, 50kg]이고 구명보트의 무게 제한이 100kg이라면 2번째 사람과 4번째 사람은 같이 탈 수 있지만 1번째 사람과 3번째 사람의 무게의 합은 150kg이므로 구명보트의 무게 제한을 초과하여 같이 탈 수 없습니다.

구명보트를 최대한 적게 사용하여 모든 사람을 구출하려고 합니다.

사람들의 몸무게를 담은 배열 people과 구명보트의 무게 제한 limit가 매개변수로 주어질 때, 모든 사람을 구출하기 위해 필요한 구명보트 개수의 최솟값을 return 하도록 solution 함수를 작성해주세요.

제한사항
  • 무인도에 갇힌 사람은 1명 이상 50,000명 이하입니다.
  • 각 사람의 몸무게는 40kg 이상 240kg 이하입니다.
  • 구명보트의 무게 제한은 40kg 이상 240kg 이하입니다.
  • 구명보트의 무게 제한은 항상 사람들의 몸무게 중 최댓값보다 크게 주어지므로 사람들을 구출할 수 없는 경우는 없습니다.
입출력 예
[70, 50, 80, 50] 100 3
[70, 80, 50] 100 3

문제 포인트

1. 가장 무거운 사람과 가장 가벼운 사람끼리 조합해서 구명보트에 태워야됨. 가장 무거운사람 +가장 가벼운사람 조합이 보트무게 제한보다 적을 경우, j++, 해주어 해당 가벼운 사람을 태워서 다음 가벼운사람이랑 조합하도록 구현,

Greedy -> 현재 선택할수 있는 최선의 선택을 반복★★

2. 문제 꼼꼼히 읽고 손코딩 해본다음에 코드로 옮기는 걸로 연습해보자...... 바로 코드짜다가 꼬이네 계속


import java.util.*;

class Solution {
    public int solution(int[] people, int limit) {
        int answer = 0;
        
        Arrays.sort(people); // {50, 50, 70, 80}
        
        int j = 0;
        
        for(int i = people.length-1; j <= i; i--){
            if(people[i] + people[j] <= limit)
                j++;
            
            answer++;
        }
        
        return answer;
    }
}

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

 

10026번: 적록색약

적록색약은 빨간색과 초록색의 차이를 거의 느끼지 못한다. 따라서, 적록색약인 사람이 보는 그림은 아닌 사람이 보는 그림과는 좀 다를 수 있다. 크기가 N×N인 그리드의 각 칸에 R(빨강), G(초록)

www.acmicpc.net


1. 현재 위치와 같은 문자가 나오는 경우 계속 DFS 로 방문하게 되는 함수 DFS
2. R과 G가 같은 문자로 인식되므로 flag 변수를 따로 생성하여 R 혹은 G인경우 flag 변수를 true 값으로 설정하여
분류하였다.


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

public class Main {

    static int RGB = 0, RGB2 = 0;
    static int N;
    static boolean flag;
    static char map[][];
    static boolean visited[][];
    static int[] dy = {1, 0, -1, 0};
    static int[] dx = {0, 1, 0, -1};

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

        N = Integer.parseInt(br.readLine());

        map = new char[N + 1][N + 1];
        visited = new boolean[N + 1][N + 1];

        for (int i = 1; i <= N; i++) {
            String tmp = br.readLine();
            for (int j = 1; j <= tmp.length(); j++) {
                map[i][j] = tmp.charAt(j - 1);
            }
        }

        for (int i = 1; i <= N; i++) {
            for (int j = 1; j <= N; j++) {
                // 방문한 적이 없으면
                if (!visited[i][j]) {
                    DFS(i, j);
                    RGB++;
                }
            }
        }
        visited = new boolean[N + 1][N + 1];

        for (int i = 1; i <= N; i++) {
            for (int j = 1; j <= N; j++) {
                // 방문한 적이 없으면
                if (!visited[i][j]) {
                    DFS2(i, j);
                    RGB2++;
                }
            }
        }

        System.out.println(RGB+" "+ RGB2);
    }

    static void DFS(int y, int x) {
        visited[y][x] = true;

        char cur = map[y][x];

        for (int i = 0; i < 4; i++) {
            int ny = y + dy[i];
            int nx = x + dx[i];

            if (ny < 0 || nx < 0 || N < ny || N < nx)
                continue;

            if (!visited[ny][nx] && map[ny][nx] == cur)
                DFS(ny, nx);
        }
    }

    static void DFS2(int y, int x) {
        visited[y][x] = true;

        char cur = map[y][x];

        // R 혹은 G 인경우는 flag = true
        if (cur == 'R' || cur == 'G')
            flag = true;
        else
            flag = false;


        for (int i = 0; i < 4; i++) {
            int ny = y + dy[i];
            int nx = x + dx[i];

            if (ny < 0 || nx < 0 || N < ny || N < nx)
                continue;

            if (visited[ny][nx])
                continue;
            
            // flag 가 true 인경우는 R,G 인경우 이므로
            if (flag && (map[ny][nx] == 'R' || map[ny][nx] == 'G')){
                DFS2(ny, nx);
            }
            else if (!flag && map[ny][nx] == 'B'){
                DFS2(ny, nx);
            }
        }
    }
}

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

 

4963번: 섬의 개수

입력은 여러 개의 테스트 케이스로 이루어져 있다. 각 테스트 케이스의 첫째 줄에는 지도의 너비 w와 높이 h가 주어진다. w와 h는 50보다 작거나 같은 양의 정수이다. 둘째 줄부터 h개 줄에는 지도

www.acmicpc.net


단순한 BFS문제이다. 동서남북만 생각해주는것이 아니라 대각선 방향까지 생각해줘야하기떄문에 
dy, dx 배열을 8가지 방향으로 나눠서 생각해야 했다. 

static 변수랑 BFS 내부에 있는 변수랑 이름을 똑같이 만들어버려서 오랫동안 헤맸다..


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

public class Main {

    static int w, h, cnt = 0;
    static int map[][];
    static boolean visited[][];
    static int[] dy = {1, 0, -1, 0, 1, 1, -1, -1};
    static int[] dx = {0, 1, 0, -1, 1, -1, 1, -1};

    public static void main(String[] agrs) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = null;
        StringBuilder sb = new StringBuilder();


        while (true) {
            st = new StringTokenizer(br.readLine());
            w = Integer.parseInt(st.nextToken()); // 너비
            h = Integer.parseInt(st.nextToken()); // 높이

            // 입력값 0,0 이 나오면 break
            if (w == 0 && h == 0) break;

            map = new int[h + 1][w + 1];
            visited = new boolean[h + 1][w + 1];

            // 입력하는대로 map 초기화
            for (int i = 1; i <= h; i++) {
                st = new StringTokenizer(br.readLine());
                for (int j = 1; j <= w; j++) {
                    map[i][j] = Integer.parseInt(st.nextToken());
                }
            }

            for (int i = 1; i <= h; i++) {
                for (int j = 1; j <= w; j++) {
                    if (!visited[i][j] && map[i][j] == 1) {
                        BFS(i, j);
                        cnt++;
                    }
                }
            }

            sb.append(cnt + "\n");

            cnt = 0;
        }

        System.out.println(sb);
    }

    static void BFS(int y, int x) {
        Queue<int[]> qu = new LinkedList<>();
        qu.offer(new int[]{y, x});

        visited[y][x] = true;

        while (!qu.isEmpty()) {
            int curY = qu.peek()[0];
            int curX = qu.peek()[1];

            qu.poll();

            for (int i = 0; i < 8; i++) {
                int ny = curY + dy[i];
                int nx = curX + dx[i];

                if (ny < 1 || nx < 1 || ny > h || nx > w)
                    continue;

                if (visited[ny][nx] || map[ny][nx] == 0)
                    continue;

                visited[ny][nx] = true;
                qu.offer(new int[]{ny, nx});
            }
        }
    }
}

Spring에서 컨트롤러를 지정해주기 위한 어노테이션은 @Controller와 @RestController가 있습니다. 전통적인 Spring MVC의 컨트롤러인 @Controller와 Restuful 웹서비스의 컨트롤러인 @RestController의 주요한 차이점은 HTTP Response Body가 생성되는 방식입니다. 이번에는 2가지 어노테이션의 차이와 사용법에 대해 알아보도록 하겠습니다.

 

 

 

1. @Controller 이해하기


[ Controller로 View 반환하기 ]

전통적인 Spring MVC의 컨트롤러인 @Controller는 주로 View를 반환하기 위해 사용합니다. 아래와 같은 과정을 통해 Spring MVC Container는 Client의 요청으로부터 View를 반환합니다.

 

 

 

 

  1. Client는 URI 형식으로 웹 서비스에 요청을 보낸다.
  2. DispatcherServlet이 요청을 위임할 HandlerMapping을 찾는다.
  3. HandlerMapping을 통해 요청을 Controller로 위임한다.
  4. Controller는 요청을 처리한 후에 ViewName을 반환한다.
  5. DispatcherServlet은 ViewResolver를 통해 ViewName에 해당하는 View를 찾아 사용자에게 반환한다.


Controller가 반환환 뷰의 이름으로부터 View를 렌더링하기 위해서는 ViewResolver가 사용되며, ViewResolver 설정에 맞게 View를 찾아 렌더링합니다.

 

 

 

 

[ Controller로 Data 반환하기 ]

하지만 Spring MVC의 컨트롤러를 사용하면서 Data를 반환해야 하는 경우도 있습니다. 컨트롤러에서는 데이터를 반환하기 위해 @ResponseBody 어노테이션을 활용해주어야 합니다. 이를 통해 Controller도 Json 형태로 데이터를 반환할 수 있습니다.

 

 

 

 

  1. Client는 URI 형식으로 웹 서비스에 요청을 보낸다.
  2. DispatcherServlet이 요청을 위임할 HandlerMapping을 찾는다.
  3. HandlerMapping을 통해 요청을 Controller로 위임한다.
  4. Controller는 요청을 처리한 후에 객체를 반환한다.
  5. 반환되는 객체는 Json으로 Serialize되어 사용자에게 반환된다.

 

 

컨트롤러를 통해 객체를 반환할 때에는 일반적으로 ResponseEntity로 감싸서 반환을 합니다. 그리고 객체를 반환하기 위해서는 viewResolver 대신에 HttpMessageConverter가 동작합니다. HttpMessageConverter에는 여러 Converter가 등록되어 있고, 반환해야 하는 데이터에 따라 사용되는 Converter가 달라집니다. 단순 문자열인 경우에는 StringHttpMessageConverter가 사용되고, 객체인 경우에는 MappingJackson2HttpMessageConverter가 사용되며, 데이터 종류에 따라 서로 다른 MessageConverter가 작동하게 됩니다. Spring은 클라이언트의 HTTP Accept 헤더와 서버의 컨트롤러 반환 타입 정보 둘을 조합해 적합한 HttpMessageConverter를 선택하여 이를 처리합니다.

 

 

 

[ @Controller 예제 코드 ]

@Controller
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @GetMapping(value = "/users")
    public @ResponseBody ResponseEntity<User> findUser(@RequestParam("userName") String userName){
        return ResponseEntity.ok(userService.findUser(user));
    }
    
    @GetMapping(value = "/users/detailView")
    public String detailView(Model model, @RequestParam("userName") String userName){
        User user = userService.findUser(userName);
        model.addAttribute("user", user);
        return "/users/detailView";
    }
}

 

 

위 예제의 findUser는 User 객체를 ResponseEntity로 감싸서 반환하고 있고, User를 json으로 반환하기 위해 @ResponseBody라는 어노테이션을 붙여주고 있습니다. detailView 함수에서는 View를 전달해주고 있기 때문에 String을 반환값으로 설정해주었습니다. (물론 이렇게 데이터를 반환하는 RestController와 View를 반환하는 Controller를 분리하여 작성하는 것이 좋습니다.)

 

 

 

 

2. @RestController 이해하기


[ RestController ]

@RestController는 @Controller에 @ResponseBody가 추가된 것입니다. 당연하게도 RestController의 주용도는 Json 형태로 객체 데이터를 반환하는 것입니다. 최근에 데이터를 응답으로 제공하는 REST API를 개발할 때 주로 사용하며 객체를 ResponseEntity로 감싸서 반환합니다. 이러한 이유로 동작 과정 역시 @Controller에 @ReponseBody를 붙인 것과 완벽히 동일합니다.

 

 

  1. Client는 URI 형식으로 웹 서비스에 요청을 보낸다.
  2. DispatcherServlet이 요청을 위임할 HandlerMapping을 찾는다.
  3. HandlerMapping을 통해 요청을 Controller로 위임한다.
  4. Controller는 요청을 처리한 후에 객체를 반환한다.
  5. 반환되는 객체는 Json으로 Serialize되어 사용자에게 반환된다.

 

 

 

[ @RestController 예제 코드 ]

@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @GetMapping(value = "/users")
    public User findUser(@RequestParam("userName") String userName){
        return userService.findUser(user);
    }

    @GetMapping(value = "/users")
    public ResponseEntity<User> findUserWithResponseEntity(@RequestParam("userName") String userName){
        return ResponseEntity.ok(userService.findUser(user));
    }
}

 

 

findUser는 User 객체를 그대로 반환하고 있습니다. 이러한 경우의 문제는 클라이언트가 예상하는 HttpStatus를 설정해줄 수 없다는 것입니다. 예를 들어 어떤 객체의 생성 요청이라면 201 CREATED를 기대할 것이지만 객체를 그대로 반환하면 HttpStatus를 설정해줄 수 없습니다. 그래서 객체를 상황에 맞는 ResponseEntity로 감싸서 반환해주어야 합니다.

출처: https://mangkyu.tistory.com/49 [MangKyu's Diary:티스토리]

Lombok 사용상 주의점(Pitfall)

Lombok은 그 강력한 기능만큼 사용상 주의를 요한다.

현재 1.16 버전 기준.

@AllArgsConstructor, @RequiredArgsConstructor 사용금지

@AllArgsConstructor, @RequiredArgsConstructor 는 매우 편리하게 생성자를 만들어주지만 개발자의 별 생각없는 꼼꼼함이 치명적 버그가 되게 만들 수 있다.

@AllArgsConstructor
public static class Order {
    private long cancelPrice;
    private long orderPrice;
}
 
// 취소금액 5,000원, 주문금액 10,000원
Order order = new Order(5000L, 10000L); 

위 클래스에 대해 자동으로 cancelPrice, orderPrice 순서로 인자를 받는 생성자가 만들어진다. 그런데 개발자가 보기에 Order 클래스인데 cancelPrice가 orderPrice보다 위에 있는것이 마음에 안들어서 순서를 다음과 같이 바꾼다고 해보자.

@AllArgsConstructor
public static class Order {
    private long orderPrice;
    private long cancelPrice;
}

이 경우, IDE가 제공해주는 리팩토링은 전혀 작동하지 않고, lombok이 개발자도 인식하지 못하는 사이에 생성자의 파라미터 순서를 필드 선언 순서에 맞춰 orderPrice,cancelPrice로 바꿔버린다. 게다가 이 두 필드는 동일한 Type 이라서 기존 생성자호출 코드에서는 인자 순서를 변경하지 않았음에도 어떠한 오류도 발생하지 않는다.

이에 의해, 위의 생성자를 호출하는 코드는 아무런 에러없이 잘 작동하는 듯 보이지만 실제로 입력된 값은 바뀌어 들어가게 된다.

// 주문금액 5,000원, 취소금액 10,000원. 취소금액이 주문금액보다 많아짐!
Order order = new Order(5000L, 10000L); // 인자값의 순서 변경 없음

이 문제는 @AllArgsConstructor와 @RequiredArgsConstructor에 둘 다 존재하며, 이에 따라 이 두 lombok 애노테이션은 사용을 금지하는 것이 좋다.

대신, 생성자를 (IDE 자동생성등으로) 직접 만들고 필요할 경우에는 직접 만든 생성자에 @Builder 애노테이션을 붙이는 것을 권장한다. 파라미터 순서가 아닌 이름으로 값을 설정하기 때문에 리팩토링에 유연하게 대응할 수 있다.

public static class Order {
    private long cancelPrice;
    private long orderPrice;
 
    @Builder
    private Order(long cancelPrice, long orderPrice) {
        this.cancelPrice = cancelPrice;
        this.orderPrice = orderPrice;
    }
}
 
// 필드 순서를 변경해도 문제 없음.
Order order = Order.builder().cancelPrice(5000L).orderPrice(10000L).build();
System.out.println(order);

필드에 붙은 애노테이션이 생성자 쪽으로 전달 안됨

  • 필드에 애노테이션이 있을 경우 자동으로 생성자로 전달이 안 되어 문제가 될 수 있다.
  • 특히 @Data,@AllArgs… 등을 사용하면 이를 잊어버리는 경우가 부지기수 이다.
  • @XXXArgsConstructor(onConstructor=@__(@원하는애노테이션)) 를 사용할 경우 생성자의 각 인자에 애노테이션이 붙는게 아니라 생성자 자체에 애노테이션이 붙는다.

무분별한 @EqualsAndHashCode 사용 자제

@EqualsAndHashCode는 상당히 고품질의 equals, hashCode 메소드를 만들어준다. 잘 사용하면 좋지만 남발하면 심각한 문제가 생긴다.

특히 문제가 되는 점은 바로 Mutable(변경가능한) 객체에 아무런 파라미터 없이 그냥 사용하는 @EqualsAndHashCode 애노테이션이다.

@EqualsAndHashCode
public static class Order {
    private Long orderId;
    private long orderPrice;
    private long cancelPrice;
 
    public Order(Long orderId, long orderPrice, long cancelPrice) {
        this.orderId = orderId;
        this.orderPrice = orderPrice;
        this.cancelPrice = cancelPrice;
    }
}
 
Order order = new Order(1000L, 19800L, 0L);
 
Set<Order> orders = new HashSet<>();
orders.add(order); // Set에 객체 추가
 
System.out.println("변경전 : " + orders.contains(order)); // true
 
order.setCancelPrice(5000L); // cancelPrice 값 변경
System.out.println("변경후 : " + orders.contains(order)); // false

위와 같이 동일한 객체임이도 Set 에 저장한 뒤에 필드 값을 변경하면 hashCode가 변경되면서 찾을 수 없게 되어버린다. 이는 @EqualsAndHashCode의 문제라기 보다는 변경가능한 필드에 이를 남발함으로써 생기는 문제이다.

따라서,

  • Immutable(불변) 클래스를 제외하고는 아무 파라미터 없는 @EqualsAndHashCode 사용은 금지한다.
  • 일반적으로 비교에서 사용하지 않는 Data 성 객체는 equals & hashCode를 따로 구현하지 않는게 차라리 낫다.
  • 항상 @EqualsAndHashCode(of={“필드명시”}) 형태로 동등성 비교에 필요한 필드를 명시하는 형태로 사용한다.
  • 실전에서는 누군가는 이에 대해 실수하기 마련인지라 차라리 사용을 완전히 금지시키고 IDE 자동생성으로 꼭 필요한 필드를 지정하는 것이 나을 수도 있다.
  • Java equals & hashCode 좋은 equals 만드는 방법
  • 막상 개발을 하다보면 온전히 Immutable 필드를 대상으로만 equals & hashCode를 만들기는 매우 어렵다. 최소한 꼭 필요하고 일반적으로 변하지 않는 필드에 대해서만 만들도록 노력해야 한다.
  • Equals Verifier를 통해 equals, hashCode 메소드 테스트를 자동으로 할 수 있다.

나는 위와 같은 이유로 Apache Commons EqualsBuilder reflectionEquals도 사용하지 말 것을 권한다.

@Data 사용금지

@Data는 파라미터 없는 @EqualsAndHashCode와 @RequiredArgsConstructor 등을 포함하는 Mutable한 데이터 클래스를 만들어주는 조합형 애노테이션이다.

바로 @EqualsAndHashCode와 @RequiredArgsConstructor를 포함하기 때문에 사용을 아예 금지하고, 차라리 다음과 같이 명시하는 것이 좋다.

@Getter
@Setter
@ToString
public class Order {
...
    // 생성자와 필요한 경우에만 equals, hashCode 직접 작성
}

개발자에게 @Data 사용시 equals, hashCode 그리고 생성자를 직접 만들어주라고 “말해봐야” 아무 소용없다. 그냥 깔끔하게 금지시키는게 낫다.

@Value 사용금지

@Value는 Immutable 클래스를 만들어주는 조합 애노테이션이지만 이 또한 @EqualsAndHashCode, @AllArgsConstructor 를 포함한다. @EqualsAndHashCode는 불변 클래스라 큰 문제가 안되지만 @AllArgsConstructor가 문제가 된다.

아예 사용을 금지시키고 다음과 같이 만드는게 낫다.

@Getter
@ToString
public class Order {
   // private final 로 여러 필드 생성
    // 생성자와 필요한 경우에만 equals, hashCode 직접 작성
}

@Builder 를 생성자나 static 객체 생성 메소드에

@Builder 를 사용하면 객체 생성이 좀 더 명확하고 쉬워지는데, 이는 기본적으로 @AllArgsConstructor를 내포하고 있다. 이 자체는 평상시에는 큰 문제가 안된다. 생성자를 package private으로 만들기 때문에 외부에서 생성자를 호출하는 일은 쉽게 안생긴다. 하지만 그래도 해당 클래스의 다른 메소드에서 이렇게 자동으로 생성된 생성자를 사용하거나 할 때 문제 소지가 있다.

따라서 @Builder 애노테이션은 가급적 클래스 보다는 직접 만든 생성자 혹은 static 객체 생성 메소드에 붙이는 것을 권장한다. (생성자 부분에 예시 나옴)

@Log

@Log 를 통해 각종 Logger 를 자동생성 할 수 있다. 이 때 기본적으로 private static final로 생성하는데, static이 아닌 필드로 만들고자 하거나 Logger 객체 이름을 변경하고자 한다면 lombok.config를 사용하면 된다.

lombok.log.fieldName=logger # 로거 객체 이름을 logger로 변경. 원래는 log
lombok.log.fieldIsStatic=false # 로거를 static이 아닌 필드로 생성

또한, 가급적이면 @Slf4j만 사용하고 나머지는 사용할 수 없게 금지시키는 것도 가능하다. 자세한 것은 아래 lombok.config 항목에서 설명한다.

나는 (라이브러리성이 아닌) 일반 서비스에서는 Logger를 field 변수로 만들 필요 없이 static final로 하는 것을 선호한다. 그래야 별다른 처리 없이 static method에서도 호출 가능하기 때문이다.

@NonNull 사용 금지

@NonNull 사용금지는 사실 개인적 취향에 가까운데, 불필요하게 branch coverage를 증가시키기 때문이다(즉, 프로젝트 코드 커버리지를 유지하고 싶다면 null인 상황에서 오류발생과 그렇지 않은 상황에 대한 테스트를 모든 사용처에서 만들어야 한다). 그렇다고 이 코드를 일일이 테스트를 만들자니 이미 검증된 라이브러리의 기능을 사용하는 곳곳에서 모두 테스트를 만드는 것도 굉장히 소모적인 일이다.

나는 그래서 @NonNull을 사용하지 않고 Guava Preconditions로 null을 검증하고 오류 처리하는 것을 선호한다. Preconditions.checkNotNull은 branch를 만들지 않는다.

이 경우 에러 메시지를 명확하게 작성하는게 가능해지는 또 다른 장점도 생긴다.

// null일 경우 "userName must not be null." 메시지로 예외 발생
Preconditions.checkNotNull(userName, "userName must not be null.")

@ToString, @EqualsAndHashCode 필드명 지정시 오타 문제

Lombok 1.18.0 부터 @ToString, @EqualsAndHashCode에 대해 필드, 혹은 메소드에 Include, Exclude 지정이 가능해졌다. 따라서 아래 문제가 모두 해소 된다.

@ToString  @EqualsAndHashCode에서는 파라미터로 특정 필드를 지정해서 처리 대상에 포함시키거(of)나 제외(exclude)시킬 수 있다.

헌데 문제는 이게 필드 이름을 String으로 지정한다는 점이다. 이로 인해 IDE 에서 필드명을 리팩토링할 때 올바로 반영이 안되거나, 아주 단순한 오타가 나도 눈치를 못 챌 수 있다. 보통 오타등으로 인해 잘못된 필드가 지정되면 Compile 시점에 warning이 출력된다. 하지만 warning일 뿐, error가 아닌지라 그마저도 모르고 넘어갈 가능성이 높다.

현재 버전에서는 이를 error로 격상시킬 방법이 없다. 컴파일러 옵션 -Werror를 주면 warning시에도 오류를 내며 컴파일을 멈추는데, 또 다른 문제는 lombok과 관계없는 다른 너무 많은 경우에 대해서 에러를 내버린다.(java 8 현재 -Xlint를 통한 옵션 미세 조정이 제대로 작동을 안함. javac 버그로 보임)

이 문제는 Jenkins Log Parser Plugin 으로 어느정도 해결 가능하다.

Lombok Field 지정이 올바른지 검사 에 관련 방법을 정리해두었다.

실무 프로젝트에서는 보수적으로 사용

개인 Toy 프로젝트가 아닌 실무 프로젝트에서는 가급적 @Getter, @Setter, @ToString 만 사용하고 그 외의 것들 사용은 자제하거나 매뉴얼을 잘 읽어보고서 보수적으로 사용한다.

@EqualsAndHashCode는 Immutable 필드만 명시적으로 지정하면서 자제해서 사용하거나 가급적 IDE 코드 제너레이션등을 통해 코드를 직접 만든다.

특히 Experimental 기능은 사용하지 말고, IDE 지원이 확실치 못하고 문법 파괴적인 val도 사용하지 않는 것이 좋을 것 같다.

lombok.config를 통해 애노테이션 사용금지 및 각종 설정

lombok 도 이렇게 일부 기능에 대해 사용금지가 필요하다고 느꼈는지, Configuration System lombok.config를 통해 다양한 설정이 가능하게 해두었다.

예를들어, 프로젝트 최상단 디렉토리에 lombok.config 파일을 만들고 다음과 같이 지정하면 @Data, @Value, val, @NonNull, @AllArgsConstructor, @RequiredArgsConstructor 등의 사용이 금지된다.

억지로 사용할 경우 컴파일 오류가 발생한다.

config.stopBubbling = true
lombok.data.flagUsage=error
lombok.value.flagUsage=error
lombok.val.flagUsage=error
lombok.var.flagUsage=error
lombok.nonNull.flagUsage=error
lombok.allArgsConstructor.flagUsage=error
lombok.requiredArgsConstructor.flagUsage=error
lombok.cleanup.flagUsage=error
lombok.sneakyThrows.flagUsage=error
lombok.synchronized.flagUsage=error
# experimental 전체 금지
lombok.experimental.flagUsage=error

# 기타 각종 사용해서는 안되는 기능들을 모두 나열할 것.

상세한 설정 옵션은 각 애노테이션 매뉴얼 페이지 하단에 자세히 나와 있다.

Lombok 라이브러리에서 제공하는 어노테이션 중에서 자주 사용되는 어노테이션 위주로 살펴보도록 하겠습니다.

접근자/설정자 자동 생성

제일 먼저 살펴볼 어노테이션은 @Getter와 @Setter 입니다. 아마 Lombok에서 가장 많이 사용되는 어노테이션일 텐데요. 예를 들어, xxx라는 필드에 선언하면 자동으로 getXxx()(boolean 타입인 경우, isXxx())와 setXxx() 메소드를 생성해줍니다.

@Getter @Setter
private String name;

위와 같이 특정 필드에 어노테이션을 붙여주면, 다음과 같이 자동으로 생성된 접근자와 설정자 메소드를 사용할 수 있어서 매우 편리합니다.

user.setName("홍길동");
String userName = user.getName();

또한, 필드 레벨이 아닌 클래스 레벨에 @Getter 또는 @Setter를 선언해줄 경우, 모든 필드에 접근자와 설정자가 자동으로 생성됩니다.

VO 클래스를 작성할 때 마다, 접근자와 설정자 메소드를 작성해주는게 참 번거로운 일이었는데, (특히, 수정할 때는 더욱이) Lombok을 쓰게되면 이런 노가다성 코딩에서 해방될 수 있습니다. :)

생성자 자동 생성

Lombok을 사용하면 생성자도 자동으로 생성할 수 있습니다. @NoArgsConstructor 어노테이션은 파라미터가 없는 기본 생성자를 생성해주고, @AllArgsConstructor 어노테이션은 모든 필드 값을 파라미터로 받는 생성자를 만들어줍니다. 마지막으로 @RequiredArgsConstructor 어노테이션은 final이나 @NonNull인 필드 값만 파라미터로 받는 생성자를 만들어줍니다.

@NoArgsConstructor
@RequiredArgsConstructor
@AllArgsConstructor
public class User {
  private Long id;
  @NonNull
  private String username;
  @NonNull
  private String password;
  private int[] scores;
}
 
User user1 = new User();
User user2 = new User("dale", "1234");
User user3 = new User(1L, "dale", "1234", null);

ToString 메소드 자동 생성

toString() 메소드를 작성하는 것도 여간 귀찮은 일이 아닙니다. 하지만 Lombok을 사용하면 @ToString 어노테이션만 클래스에 붙여주면 자동으로 생성해줍니다.

예제와 같이 exclude 속성을 사용하면, 특정 필드를 toString() 결과에서 제외시킬 수도 있습니다.

 
@ToString(exclude = "password")
public class User {
  private Long id;
  private String username;
  private String password;
  private int[] scores;
}

위와 같이 클래스에 @ToString 어노테이션을 붙이고, 아래와 같이 필드를 세팅 후 출력을 하면,

User user = new User();
user.setId(1L);
user.setUsername("dale");
user.setUsername("1234");
user.setScores(new int[]{80, 70, 100});
System.out.println(user);

다음과 같이, 클래스명(필드1명=필드1값,필드2명=필드2값,...) 식으로 출력됩니다.

User(id=1, username=1234, scores=[80, 70, 100])

Lombok을 사용하기 전에는 Apache Commons Lang 라이브러리의 ToStringBuilder를 사용했었는데, Lombok을 사용하는 게 어노테이션 한 방으로 끝나니 훨씬 편한 것 같습니다.

equals, hashCode 자동 생성

자바 빈을 만들 때 equals와 hashCode 메소드를 자주 오버라이딩 하는데요. @EqualsAndHashCode 어노테이션을 사용하면 자동으로 이 메소드를 생성할 수 있습니다.

@EqualsAndHashCode(callSuper = true)
public class User extends Domain {
  private String username;
  private String password;
}
 

callSuper 속성을 통해 equals와 hashCode 메소드 자동 생성 시 부모 클래스의 필드까지 감안할지 안 할지에 대해서 설정할 수 있습니다.

즉, callSuper = true로 설정하면 부모 클래스 필드 값들도 동일한지 체크하며, callSuper = false로 설정(기본값)하면 자신 클래스의 필드 값들만 고려합니다.

User user1 = new User();
user1.setId(1L);
user1.setUsername("user");
user1.setPassword("pass");

User user2 = new User();
user1.setId(2L); // 부모 클래스의 필드가 다름
user2.setUsername("user");
user2.setPassword("pass");

user1.equals(user2);
// callSuper = true 이면 false, callSuper = false 이면 true

끝판왕! @Data

@Data는 위에서 설명드린 @Getter, @Setter, @RequiredArgsConstructor, @ToString, @EqualsAndHashCode을 한꺼번에 설정해주는 매우 유용한 어노테이션입니다.

@Data
public class User {
  // ...
}

사용 방법은 다른 어노테이션들과 대동소이합니다. 클래스 레벨에서 @Data 어노테이션을 붙여주면, 모든 필드를 대상으로 접근자와 설정자가 자동으로 생성되고, final 또는 @NonNull 필드 값을 파라미터로 받는 생성자가 만들어지며, toStirng, equals, hashCode 메소드가 자동으로 만들어집니다.

이상으로 자주 사용되는 Lombok 어노테이션에 대해서 살펴보았습니다. 다음 포스팅에서는 많이 알려지지는 않았지만 알아두면 유용한 Lombok 어노테이션에 대해서 알아보겠습니다.

+ Recent posts