시작하며

Leetcode 190. Reverse Bits 문제를 풀면서 Java에서의 int to binary , binary to int 처리와 관련하여 궁금한 부분이 생겨서 정리하게 되었습니다.

 

Integer.parseInt(String s, int radix)

 

자바에서는 Integer 클래스의 parseInt() 메서드로 2진수를 10진수로 쉽게 변환할 수 있습니다.

파라미터로 [param1] String 타입의 binary와 [param2] int 타입의 진수 값(= radix, 몇 진수인지)를 넣어주면 됩니다.

  • param1(String s) : "11111111111111111111111111111101”
  • param2(int radix) : 2

 

unsigned 타입을 지원하지 않는 Java

 

하지만 사용 시 주의해야 할 점이 있습니다.

int n = Integer.parseInt("11111111111111111111111111111101", 2);

 

위와 같은 코드를 작성하게 되면 NumberFormatException 에러가 발생합니다.

java.lang.NumberFormatException: For input string: "10111111111111111111111111111111" under radix 2 
at line 67, java.base/java.lang.NumberFormatException.forInputString

 

이유는 Java에서 기본적으로 unsigned 타입을 지원하지 않기 때문입니다.

(Java의 primitive 타입은 단순하게 signed 타입만 지원합니다.)

 

 

Integer.parseInt() 메서드의 일부를 살펴보면

boolean negative = false;
int i = 0, len = s.length();
int limit = -Integer.MAX_VALUE; // -2,147,483,647

if (len > 0) {
    char firstChar = s.charAt(0);
    if (firstChar < '0') { // Possible leading "+" or "-"
        if (firstChar == '-') {
            negative = true;
            limit = Integer.MIN_VALUE; // -2,147,483,648
        } else if (firstChar != '+') {
            throw NumberFormatException.forInputString(s, radix);
        }

        if (len == 1) { // Cannot have lone "+" or "-"
            throw NumberFormatException.forInputString(s, radix);
        }
        i++;
    }
    int multmin = limit / radix;
    int result = 0;
    while (i < len) {
        // Accumulating negatively avoids surprises near MAX_VALUE
        int digit = Character.digit(s.charAt(i++), radix);
        if (digit < 0 || result < multmin) {
            throw NumberFormatException.forInputString(s, radix);
        }
        result *= radix;
        if (result < limit + digit) {
            throw NumberFormatException.forInputString(s, radix);
        }
        result -= digit;
    }
    return negative ? result : -result;
} else {
    throw NumberFormatException.forInputString(s, radix);
}
  • 내부 로직에 따라 "11111111111111111111111111111101" binary string이 while문 안에서 signed int 값의 범위를 벗어나게 되어 예외가 발생하게 됩니다.
  • 구체적으로
    • while 루프 내에서 숫자를 누적(= 음수 누적)하여 계산하는데,
    • 곱셈과 뺄셈을 하기 전에 미리 overflow 검사를 하여 signed int 범위를 초과할 경우 NumberFormatException을 던지고 있습니다.
    • overflow 검사는 [곱셈]의 경우 [result < multmin] 조건으로, [뺄셈]의 경우 [result < limit + digit] 조건으로 수행하는데 풀어서 설명하면 각 조건의 의미는 다음과 같습니다.
    • [곱셈] result < multmin
      • multmin = limit / radix; 이므로 결국 result < limit / radix 
      • 양 변에 radix를 곱하면 result * radix < limit
      • 즉, 곱한 값(result * radix)이 limit을 넘어서면 NumberFormatException 예외 발생
    • [뺄셈] result < limit + digit 
      • 양 변에 digit을 빼면 result - digit < limit
      • 즉, 뺀 값(reulst - digit)이 limit을 넘어서면 NumberFormatException 예외 발생
  • signed int 와 unsigned int의 범위는 아래와 같습니다.
    • signed int 범위 : -2,147,483,648 ~ 2,147,483,647
    • unsigned int 범위 : 0 ~ 4,294,967,295
  • 결론적으로, binary string "11111111111111111111111111111101"는 "4294967293"를 향하여 값이 변환(누적)되었고, 해당 값은 signed int로는 표현할 수 없는(= signed int 범위에서 벗어나는) 값이어서 예외가 발생한 것입니다.

디버깅 화면. 32번째 계산에서 범위를 벗어나 예외 처리됩니다.

 

 

 

Java에는 unsigned int가 없는데 해당 binary string을 어떻게 처리할까? : parseUnsignedInt()

 

Java 8 버전 이후에서는 "11111111111111111111111111111101"와 같이 signed int 범위를 벗어나는 binary 값을 처리할 수 있도록  unsigned Integer API를 제공합니다.

parseUnsignedInt() 메서드를 사용하면 해당 binary string도 정상적으로 parsing이 됩니다. 하지만 막상 parsing된 값을 출력해보면 (2의 보수 계산을 통한) signed int 값이 출력되는데요.

int n = Integer.parseUnsignedInt("11111111111111111111111111111101", 2);
System.out.println(n); // -3

 

추가적으로 toUnsignedString() 메서드를 사용하면 unsigned int에 해당하는 값을 String 타입으로 출력할 수 있습니다.

int n = Integer.parseUnsignedInt("11111111111111111111111111111101", 2);
String unsignedString = Integer.toUnsignedString(n); 
System.out.println(unsignedString); // 4294967293

 

얼핏 봐서는 parseUnsignedInt() 메서드를 통해 unsigned int 범위에 해당하는 값이 임시적으로 signed int 값(=-3)으로 저장이 되고, 임시적으로 저장되었던 signed int 값이 toUnsignedString() 메서드를 통해 (String 타입의) unsigned int 값(=4294967293)으로 출력되는 것처럼 보입니다.

 

 

왜 이렇게 동작하는지는 parseUnsignedInt() 메서드 내부를 살펴보면 알 수 있습니다.

public static int parseUnsignedInt(String s, int radix)
          throws NumberFormatException {
  if (s == null)  {
      throw new NumberFormatException("Cannot parse null string");
  }

  int len = s.length();
  if (len > 0) {
      char firstChar = s.charAt(0);
      if (firstChar == '-') {
          throw new
              NumberFormatException(String.format("Illegal leading minus sign " +
                                                 "on unsigned string %s.", s));
      } else {
          if (len <= 5 || // Integer.MAX_VALUE in Character.MAX_RADIX is 6 digits
              (radix == 10 && len <= 9) ) { // Integer.MAX_VALUE in base 10 is 10 digits
              return parseInt(s, radix);
          } else {
              long ell = Long.parseLong(s, radix);
              if ((ell & 0xffff_ffff_0000_0000L) == 0) {
                  return (int) ell;
              } else {
                  throw new
                      NumberFormatException(String.format("String value %s exceeds " +
                                                          "range of unsigned int.", s));
              }
          }
      }
  } else {
      throw NumberFormatException.forInputString(s, radix);
  }
}

 

첫 번째로 null 체크를 하고,

두 번째로 첫 문자가 '-'인지(음수인지) 체크합니다. 음수는 unsigned int가 될 수 없기 때문입니다.

그 다음에는 입력 string 길이에 따라 분기처리를 하는데요. 5자리 이하 짧은 문자열이면 parseInt() 메서드로 처리하고, 그 이상(= 꽤 큰 값)이면 parseLong() 메서드로 처리하여 결과를 long 타입의 변수(ell)에 저장합니다.

parseLong() 메서드는 parseInt() 메서드와 동작 방식이 유사하지만 limit 값은 훨씬 크기 때문에, 위에서 parseInt() 메서드가 예외처리 했던 "signed int로는 표현할 수 없는, signed int 범위에서 벗어나는 값(=4294967293)"을 충분히 표현하고 처리할 수 있습니다. 

 

if ((ell & 0xffff_ffff_0000_0000L) == 0) { ... }

 

그렇게 처리된 long 타입(ell)의 값을 '상위 32비트만 1로 설정한 64비트 마스크'를 사용해, 해당 값의 상위 32비트가 모두 0인지 확인하는 작업을 거칩니다.

  • & 는 '비트 AND 연산자' 입니다. 두 비트가 모두 1일때만 1을 반환합니다. 그렇지 않으면 0을 반환합니다.
  • 0xFFFFFFFF00000000L(16진수 마스크)는 2진수로 표현하면 11111111_11111111_11111111_11111111_00000000_00000000_00000000_00000000 가 됩니다.
  • 즉 ell의 상위 32비트가 모두 0이어야만 해당 조건을 만족하게 됩니다.

이 검증 과정을 통과해야만, long 타입에 임시 저장된 unsigned int 범위의 값(= 4294967293)이 다시 안전하게 int 타입으로 형변환(= -3)될 수 있는 것입니다.

 

정리하자면, 이 과정은 상당히 클 것으로 예상되는 값(="11111111111111111111111111111101")을 long 타입으로 파싱(= 4294967293)한 뒤, 다시 int로 형변환(=-3)하는 과정으로 볼 수 있습니다.

 

디버깅 화면. long ell 값은 '4294967293' 이고 (int) ell 값은 '-3' 입니다.

 

 

마지막으로 toUnsignedString() 메서드 내부도 살펴보면

public static String toUnsignedString(int i) {
    return Long.toString(toUnsignedLong(i));
}

public static long toUnsignedLong(int x) {
    return ((long) x) & 0xffffffffL;
}

 

toUnsignedString() 메서드는 내부적으로 toUnsignedLong() 메서드의 반환값을 String으로 변환하여 리턴하고 있습니다.

toUnsignedLong() 메서드는 앞서 본 내용과 유사하게 '0xffffffffL' 마스크를 사용하여 값을 변환하고 있습니다.

  • '0xffffffffL'는 이진수로 표현하면 00000000_00000000_00000000_00000000_11111111_11111111_11111111_11111111 이 됩니다
    • 0xffffffff = 32개의 비트가 전부 1인 수
    • 맨 뒤에 L이 붙어서 long 리터럴이 됨 (64비트)
    • 즉, '0x00000000FFFFFFFF' (64비트)
  • 이 마스크는 signed int(특히 '음의 정수')를 unsigned int로 변환하는데 유용하게 사용됩니다. 예를 들어
  • x = -1 일 때
    • [입력 값] -1 은 
      • (32비트에서) '0xFFFFFFFF' 이고 
      • (64비트에서) long 부호 확장을 통해 '0xFFFFFFFFFFFFFFFF'가 됩니다.
    • [마스크] 0xffffffffL 는
      • (64비트) '0x00000000FFFFFFFF' 입니다.
    • 즉, 0xFFFFFFFFFFFFFFFF & 0x00000000FFFFFFFF = 0x00000000FFFFFFFF = '4294967295L' 입니다
  • x = -2 일 때
    • [입력 값] -2 는
      • (32비트에서) '0xFFFFFFFE' 이고
      • (64비트에서) long 부호 확장을 통해 '0xFFFFFFFFFFFFFFFE'가 됩니다.
    • [마스크] 0xffffffffL 는
      • (64비트) '0x00000000FFFFFFFF' 입니다.
    • 즉, 0xFFFFFFFFFFFFFFFE & 0x00000000FFFFFFFF = 0x00000000FFFFFFFE = '4294967294L' 입니다.

정리하자면, 이 과정은 int 값(특히 signed int, 음의 정수)을 unsigned int 로 표현하여 long 타입으로 변환하는 과정 (그리고 long 타입 값을 String으로 변환하는 과정)으로 볼 수 있습니다.

 

 

정리하며

Integer.parseInt() 메서드는 binary string 을 32비트 signed int로 해석하여 parsing을 진행합니다. Java는 기본적으로 signed 타입만 제공하기 때문입니다. 따라서 parsing을 진행하면서 해당 binary가 signed int 의 범위를 벗어나는 것으로 판단되면 예외를 발생시킵니다.

 

이때 사용할 수 있는 것이 Integer.parseUnsignedInt() 메서드입니다. 해당 메서드는 동일한 binary string을 unsigned int로 해석하여 부호 없는 32비트 정수 범위 값(0 ~ 4,294,967,295)로 변환해줍니다. 내부적으로는 Long.parseLong()을 통해 64비트 long으로 parsing하고, 그 값의 상위 32비트가 0일 경우 int로 형변환하여 반환하는 것입니다.

 

 

참고

 

문제

주어진 연결 리스트에서 인접한 두 노드씩 교환한 후, 변경된 리스트의 헤드를 반환하라.
단, 노드의 값을 직접 바꾸면 안 되고, 노드 자체를 교환해야한다.

https://leetcode.com/problems/swap-nodes-in-pairs/description/

 

public class SwapNodesInPairs {

    public static class ListNode {
        int val;
        ListNode next;
        ListNode(int val) { this.val = val; }
        ListNode(int val, ListNode next) { this.val = val; this.next = next; }
    }

    public static void main(String[] args) {

        ListNode node = new ListNode(1);
        node.next = new ListNode(2);
        node.next.next = new ListNode(3);
        node.next.next.next = new ListNode(4);
        node.next.next.next.next = new ListNode(5);
        node.next.next.next.next.next = new ListNode(6);

        ListNode result = swapNodesInPairs(node); // 구현
        while(result != null) {
            System.out.println(result.val);
            result = result.next;
        }
    }
    
}

 

 

해결1) 반복문 사용

1-1) while문과 Dummy Node 사용 1

public static ListNode swapNodesInPairs(ListNode head) {

        ListNode node = new ListNode(0);
        node.next = head;

        ListNode root = node;

        while (node.next != null && node.next.next != null) {

            ListNode a = node.next;
            ListNode b = node.next.next;

            // swap
            a.next = b.next;
            node.next = b;
            node.next.next = a;

            // move
            node = node.next.next;
        }

        return root.next;
    }

 

while문과 Dummy Node (= node, ListNode(0)) 을 사용하여 두 노드의 쌍을 swap 할 수 있습니다.
인자로 넘겨 받은 연결 리스트의 head 노드를 Dummy Node (node) 뒤에 연결하고 

node 를 기준으로 그 다음 노드(a) 와 그 다음 다음 노드(b) 의 포인터를 swap 합니다.

swap 이 완료되면 다음 두 노드의 쌍을 swap 해야 하므로 두 노드 뒤로 이동합니다.

 

조금 더 구체적으로 이야기하면,

(1) a 노드의 다음을 b 노드의 다음으로 연결한다

(2) 기준 노드의 다음을 b로 연결한다

(3) 기준 노드의 다음 다음(b.next)을 a로 연결한다.

 

1-2) while문과 Dummy Node 사용 2

public static ListNode swapNodesInPairsAnotherAnswer(ListNode head) {

        ListNode node = new ListNode(0);
        node.next = head;

        ListNode root = node;

        while (node.next != null && node.next.next != null) {

            ListNode a = node.next;
            ListNode b = node.next.next;

            // swap
            a.next = b.next;
            b.next = a;
            node.next = b;

            // move
            node = node.next.next;
        }

        return root.next;
    }

 

while 문과 Dummy Node(= node, ListNode(0)) 을 사용하여 swap 하는 또 다른 방식의 풀이입니다.

위 방식과 크게 다르지 않습니다. (순서만 다릅니다)

다만 swap 부분에서 node를 기준으로 연결 관계(=포인터)를 변경하는 것이 아니라,

a 노드와 b노드의 연결 관계를 swap 한 뒤(b -> a) 맨 앞의 기준 노드(node)의 다음을 b로 변경하는 것입니다.

 

구체적으로 살펴보면,

(1) a 노드의 다음을 b 노드의 다음으로 연결한다

(2) b 노드의 다음을 a 노드로 연결한다

(3) 기준 노드의 다음을 b 노드로 연결한다

 

그리고 동일하게 swap이 완료되면 두 노드 뒤로 이동합니다.

 

해결2) 재귀함수 사용

public static ListNode swapNodesInPairs(ListNode head) {

        if (head == null || head.next == null) {
            return head;
        }

        // swap
        ListNode tmp = head.next;
        head.next = swapNodesInPairs(head.next.next); // recursive call
        tmp.next = head;

        return tmp;

    }

 

while 문(반복문)을 사용하지 않고 재귀 함수를 사용하여 해결할 수도 있습니다.

코드만 봐서는 직관적으로 이해하기가 다소 어렵기 때문에 풀어서 설명하겠습니다.

 

| LinkedList

1 -> 2 -> 3 -> 4 -> 5 -> 6 -> null

 

| STEP1 : 시작

head = 1, head.next = 2, swapNodesInPairs(3)

tmp = head.next(2)

head(1).next = swapNodesInPairs(3) //  ↖︎ 재귀 호출 결과 : 4 -> 3 -> 5 -> 6 -> null (* 1 -> 4 -> 3 -> 6 -> 5 -> null 완성 *)

 

    | STEP2 : swapNodesInPairs(3) 내부 로직

   head = 3, head.next = 4, swapNodesInPairs(5)

    tmp = head.next(4)

    head(3).next = swapNodesInPairs(5) // ↖︎ 재귀 호출 결과 : 6 -> 5 -> null (* 3 -> 6 -> 5 -> null 완성 *)

 

             | STEP3 : swapNodesInPairs(5) 내부 로직

           head = 5, head.next = 6, swapNodesInPairs(null)

             tmp = head.next(6)

             head(5).next = swapNodesInPairs(null) //  ↖︎ 재귀호출 결과 : null (* 5 -> null 완성 *)

 

                          | STEP4 : swapNodesInPairs(null) 내부로직

                          head = null 

                           if (head == null) -> return head // (null) 리턴 ↖︎

 

 

            tmp(6).next = head(5)

            return tmp // (6 -> 5 -> null) 리턴 ↖︎

 

 

    tmp(4).next = head(3)

    returm tmp  // (4 -> 3 -> 6 -> 5 -> null) 리턴 ↖︎

 

 

tmp(2).next = head(1)

return tmp // 최종 결과로 (2 -> 1 -> 4 -> 3 -> 6 -> 5 -> null) 리턴

 

 

마무리하며

재귀 함수는 코드로 얼핏 보았을 때는 간단해보이지만, 한번에 생각해내거나 이해하기가 생각보다 까다로운 것 같습니다. 다음 함수 호출에 변수의 값은 어떻게 할당되고, 최종 리턴 값은 어떻게 만들어지는지 그리고 리턴 결과를 호출부에서 어떻게 활용하는지 그림으로 그려보는 것이 이해하는데 큰 도움이 되는 것 같습니다.

 

그리고 swap을 할 때에는 꼭 이전 변수의 값을 미리 담아둘 임시 변수가 필요하다는 것을 기억해두면 좋을 것 같습니다.

'PS > Leetcode' 카테고리의 다른 글

15. 세 수의 합(3 Sum)  (0) 2025.03.13

문제


정수 배열 nums가 주어졌을 때, 다음 조건을 만족하는 모든 세 수의 조합 [nums[i], nums[j], nums[k]]을 반환하라.

* i != j, i != k, j != k (서로 다른 인덱스를 가져야 함)

* nums[i] + nums[j] + nums[k] == 0 (세 수의 합이 0이 되어야 함)

* 단, 중복된 조합은 포함하지 않아야 한다.

https://leetcode.com/problems/3sum/description/

 

예시

Example 1:

Input: nums = [-1,0,1,2,-1,-4]
Output: [[-1,-1,2],[-1,0,1]]

Explanation: 
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0.
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0.
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0.
The distinct triplets are [-1,0,1] and [-1,-1,2].
Notice that the order of the output and the order of the triplets does not matter.

Example 2:

Input: nums = [0,1,1]
Output: []

Explanation: The only possible triplet does not sum up to 0.

Example 3:

Input: nums = [0,0,0]
Output: [[0,0,0]]

Explanation: The only possible triplet sums up to 0.

 

제약

* 3 <= nums.length <= 3000
* -10^5 <= nums[i] < = 10^5  

 

1) Brute Force 풀이

import java.util.*;
public class ThreeNumberSum01TimeOut {
    public static void main(String[] args) {

        int[] nums = {-1, 0, 1, 2, -1, -5};
        List<List<Integer>> result = threeNumberSum(nums);
        System.out.println(result);

    }

    // 1 - Brute Force => O(N^3)
    public static List<List<Integer>>threeNumberSum(int[] nums) {

        Arrays.sort(nums); // {-5, -1, -1, 0, 1, 2}; => O(NlogN)

        List<List<Integer>> result = new LinkedList<>();

        for (int i = 0; i < nums.length - 2; i++) {
            
            // 중복 값 예외처리
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            
            for (int j = i + 1 ; j < nums.length - 1; j++) {
                
                // 중복 값 예외처리
                if (j > i + 1 && nums[j] == nums[j - 1]) {
                    continue;
                }

                for (int k = j + 1; k < nums.length; k++) {
                    
                    // 중복 값 예외처리
                    if (k > j + 1 && nums[k] == nums[k - 1]) {
                        continue;
                    }

                    if (nums[i] + nums[j] + nums[k] == 0) {
                        result.add(Arrays.asList(nums[i], nums[j], nums[k]));
                    }
                }
            }
        }

        return result;
    }
}

 

가장 단순한 방법으로 '완전탐색(Brute Force)'을 적용해볼 수 있다. 

단, 문제에 '중복된 조합은 포함되어야 하지 않아야 한다'는 조건이 있으므로  중복 값 예외처리 하는 로직이 필요하다.

// 중복 값 예외처리
if (i > 0 && nums[i] == nums[i - 1]) {
    continue;
}

 

배열이 정렬된 상태에서는 '연속된 값의 중복 여부'를 판단하여 데이터 중복 저장을 방지할 수 있다.

그림에서 볼 수 있듯이 이중 혹은 삼중 Loop를 순회할 때, 연속된 인덱스에 동일한 값이 나타나면 조합이 중복해서 발생하게 된다.

연속된 값을 비교하여 중복된 값이 등장했을 때, continue 키워드를 사용하여(= 다음번 인덱스의 원소로 skip) 간단히 중복 조합 저장을 방지할 수 있다.

 

하지만 삼중 Loop의 시간복잡도는 O(N^3)이고 문제 제약 조건에서 최대 배열의 길이가 3000이므로 해당 풀이는 timeout이 발생한다.

 

2) 투 포인터를 활용한 풀이

import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

public class ThreeNumberSum02 {
    public static void main(String[] args) {

        int[] nums = {-1, 0, 1, 2, -1, -5};
        List<List<Integer>> result = threeNumberSum(nums);
        System.out.println(result);

    }

    // 1 - Two Pointer => O(N^2)
    public static List<List<Integer>>threeNumberSum(int[] nums) {

        int left, right, sum;
        Arrays.sort(nums); // {-5, -1, -1, 0, 1, 2}; => O(NlogN)
        List<List<Integer>> result = new LinkedList<>();

        for (int i = 0; i < nums.length - 2; i++) {
            // 중복 값 skip
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }

            // 이후부터 two pointer로 탐색
            left = i + 1;
            right = nums.length - 1;
            while (left < right) {
                sum = nums[i] + nums[left] + nums[right];
                if (sum < 0) {
                    left++;
                }
                else if (sum > 0) {
                    right--;
                }
                else { // sum == 0

                    result.add(Arrays.asList(nums[i], nums[left], nums[right]));
                    // *중복값 건너뛰기*
                    while (left < right && nums[left] == nums[left + 1]) {
                        left++;
                    }
                    while (left < right && nums[right] == nums[right - 1]) {
                        right--;
                    }

                    // 정답이 나왔으므로 투 포인터 모두 한 칸씩 이동
                    // 합이 0인 상황이므로, 양쪽 모두 이동 필요!
                    left++;
                    right--;
                }
            }

        }

        return result;
    }
}

 

투 포인터를 활용한 풀이는 다음과 같다.

 

0. 배열을 오름차순으로 정렬

 

1. 연속적으로 중복된 원소를 제외하고(-> 중복된 조합 저장 방지), 각 원소를 순회하며 다음에 나오는 값들에 대해서 '투 포인터'를 적용

  • i : 기준이 되는 원소
  • left : 투 포인터 중 왼쪽 포인터
  • right : 투 포인터 중 오른쪽 포인터

2. 세 값의 합(nums[i] + nums[left] + nums[right])을 '0'과 비교하여 투 포인터 조정

  • sum < 0 이면, left 한칸 이동 (=> sum 값을 키움)
  • sum > 0 이면, right 한칸 이동 (=> sum 값을 줄임)
  • sum == 0 이면, 
    • 1) result 리스트에 값 저장
    • 2) 투 포인터 중 left 부분에서 연속된 중복 값이 있는지 체크 -> 중복 조합 저장 방지
    • 3) 투 포인터 중 right 부분에서 연속된 중복 값이 있는지 체크 -> 중복 조합 저장 방지
    • 4) 정답이 나왔으므로 양쪽 투 포인터 모두 한칸 이동
      • WHY) 왜 두 포인터 모두 한 칸씩 이동해야 하는가? 한 쪽만 이동해서 확인해야 하는 경우도 있지 않을까?
      • ANSWER) 두 값이 고정된 상태에서 하나만 이동했을 때 또 다시 정답이 나오는 경우는 '중복된 데이터'인 경우 밖에 없음. 하지만 그러한 경우는 문제 조건에 의해 정답이 될 수가 없다.

 

 

이렇게 투 포인터를 활용하면 

 

  • 오름차순 정렬 -> O(NlogN)
  • 배열의 각 원소를 FOR LOOP로 순회 -> O(N)
    • 투 포인터가 While 문 안에서 left부터 right 까지 순회 -> O(N)

 

최종적으로 O(N^2) 시간 안에 동작할 수 있게 된다.

 

'PS > Leetcode' 카테고리의 다른 글

24. Swap Nodes in Pairs  (0) 2025.03.29

이슈 상황

“com.docker.vmnetd” will damage your computer.

 

갑자기 위와 같은 에러 메시지가 발생함.

이런 저런 방법으로 docker 삭제를 시도했으나 완전히 삭제하는데 실패함.

 

 

해결

brew uninstall --cask docker --force
brew uninstall --formula docker --force
brew install --cask docker
brew install docker-compose

 

위 방법으로 docker를 완전히 삭제 후 재설치함

 

재설치시 Trash Bin에 있던 내용들, 기존 Downloads에 있던 Docker.dmg 모두 삭제 후 새로운 파일들로 재설치 진행함.

 

 

References

https://github.com/docker/for-mac/issues/7046

 

"Docker is damaged and can't be opened" error · Issue #7046 · docker/for-mac

Description About once a week my docker for mac installation throws an error and wants me to delete it. This forces me to have to redownload and install Docker for mac. I get a MacOS alert with the...

github.com

 

https://github.com/docker/for-mac/issues/7520

 

[Workaround in description] Mac is detecting Docker as a malware and keeping it from starting · Issue #7520 · docker/for-mac

Description Whenever Docker is started, this error is shown: Malware Blocked. “com.docker.socket” was not opened because it contains malware. this action did not harm your Mac. Reproduce Start Dock...

github.com

 

이미지 - 스타터스 취업 부트캠프(STARTERS) 공식 블로그

0. 마무리

서울시 중구 청계천로 24 케이스퀘어시티빌딩 웅진씽크빅 청계사옥 (이미지 - 네이버 지도)
파이널 프로젝트 발표가 진행됐던 웅진씽크빅 청계사옥 지하 2층 문봉교실 (이미지 - 유데미 스타터스 제공)

  • 드디어 길고 길었던 3개월 간의 부트캠프 일정이 모두 끝이 났다. 첫 번째 달에는 HTML, CSS, Javascript(와 리액트 아주 살짝), Java를 배웠고, 두 번째 달에는 Spring과 Git, AWS에 대해서 학습했다. (중간에 살짝 미니 프로젝트도 진행했다.) 마지막 달에는 에자일 방법론을 학습하였고 바로 파이널 프로젝트를  진행했다. (최종 테스트까지 포함해서 시험도 총 4~5번은 본 것 같다!)
  • 이렇게 나열하고 보니 정말 많은 것을 배웠고 많은 것들을 해낸 것 같아서 괜히 뿌듯하다. 결과와 상관 없이 나 자신에게 정말 고생 많았다고 말해주고 싶고, 모든 일정을 끝까지 함께 해준 동료들에게도 너무너무 고맙다고 전하고 싶다. 비록 우리의 동행은 3월 3일부로 끝이 났지만, 반드시 모두 현업에서 다시 만날 거라고 믿어 의심치 않는다!! 다들 실력 있고 열정 넘치는 개발자들이니까!!!
  • 다들 정말 정말 고생 많으셨습니다!!!!!!! 그리고 정말 재밌게 즐기다 갑니다!!!

1. 좋았던 점(Liked)

파이널 프로젝트

  • 우리 팀의 케미는 정말 완벽에 가까웠다. 초반에는 각자 이리저리 튀기도 하고 의견 차이가 쉽게 좁혀지지 않아서 서로 기분이 상하는 일도 있었다. 하지만 초반에 잠깐 이러한 과정을 겪고 나니 서로에 대한 이해도 높아지고 자연스럽게 서로가 서로를 이해하고 배려하면서 협업을 하게 되었다. (중간에 두 번 정도의 회식을 가졌는데 혹시 그 덕분이었을까...?) 그리고 이러한 팀워크를 가질 수 있었던 가장 큰 이유는 팀원 모두가 인턴에 대한 부담감과 집착을 버리고 '우리가 결국 하고 싶은 것', '우리가 최종적으로 만들어내고 싶은 것'에 대해 합의하고 그것에만 집중했기 때문이라고 생각한다. 결국 '잘 해야만 한다'는 부담감에서 벗어나 '우리가 진짜 하고 싶은 것을 한다'는 설렘과 기대감을 가지고 프로젝트에 임했던 것 같다.
  • '우리가 진짜 하고 싶은 것 / 최종적으로 만들어내고 싶은 것'에 대한 합의가 이루어지자 다른 여러 가지 부분들도 동시에 명확해졌다. 우리가 사용하고자 하는 기술 스택에 대한 제약이 사라졌으며, 여러 가지 과정과 절차에 '우리다움/ 우리생각'들이 입혀지면서 합의도 자연스럽게 이루어졌다. 항상 자기만의 언어와 방식으로 이야기하는 것을 좋아하는(잘하는) 팀장님과 도전하기를 좋아하는 팀원들의 영향력과 역할이 매우 컸다고 생각한다. 정말 즐겁고 색다른 경험이었고 많이 배울 수 있어서 너무 감사했다!
  • 뿐만 아니라 서로 하고 싶었던 부분과 잘 하는 부분이 적절히 잘 나뉘어 있었다. 나는 개인적으로 이전에 해보지 못했던 부분을 해보고 싶었는데 아니나 다를까 회원가입/로그인 부분에서 소셜 계정과 연동하는 부분을 맡게 되었다. 해당 기능에는 spring security와 oauth 2.0 기술이 사용되었는데 둘 다 처음 배워보고 적용해보는 나로서는 다소 부담이 되기도 했지만 다른 한편으로는 도전의식을 자극했다. 결과적으로 쪼금 고생을 하기는 했지만 옆에서 응원해주고 도움을 주는 팀원들 덕분에 끝내 구현을 할 수 있었고 최종 발표에서도 이에 대해 언급할 수 있어서 무척 만족스러웠다. 게다가 이러한 과정을 거치면서 개발에 대한 자신감도 정말 많이 늘은 것 같다!

우리가 제작한 서비스(Dodu)의 최종 발표 자료 일부

열정 넘치는 동료들과 강사님

  • 사실 커리큘럼 자체가 매우 좋았다고는 말하기 힘들다.왜냐면 스프링 자체가 단기간(특히 1개월은 말이 안 된다..)에 완벽하게 배워서 사용할 수 있는 프레임워크가 아니고,  요새는 개발이 전부 프론트단과 RESTful하게 통신하는 방식으로 이루어지는데 이에 대한 교육적인 고려와 구성, 지원이 부족하다고 생각했다.
  • 하지만 그럼에도 불구하고 열심히 배우고 더 많이 배우려는 사람들이 많았던 것이 정말 좋았다. 사실 나도 3개월 동안 지치고 힘들고 하기 싫은 때가 종종 있었는데 옆에서 같이 공부하는 동료들을 보고 이겨냈다. 사람들마다 위기도 있었고 각자의 우여곡절이 있었지만 결국 서로를 바라보고 서로에게 힘을 얻으며 다들 견뎌낸 것 같다. 정말 다들 존경하고 감사하고 모두가 잘 되었으면 좋겠다!!! (다들 분명 잘 될 거다... 두고 봐라!!)
  • 그리고 거의 처음이자 마지막으로 우리의 교육을 담당했던 강사님과 사적인 이야기를 해볼 수 있는 기회가 있었다. 최종 발표를 끝내고 오후에 팀별 회고를 하는 시간에 강사님께서 각 팀을 돌아다시니면서 소감을 말씀해주시고 조언도 해주셨기 때문이다.  짧은 기간 내에 정말 많은 내용을 가르치셔야 했기 때문에 내 기억 속의 강사님은 끊임없이 공부하시고 수업을 준비하시느라 바쁘신 분이셨다. 그러나 이 날만큼은 강사님께서 친히 오셔서 개개인에 대한 인상과 소감 그리고 조언 같은 것을 해주셨는데, 생각보다 학생들 개개인에 대해서 빠삭하게(?) 파악하고 계셔서 놀랐다. 직접 말씀은 하시지 않으셨지만 그만큼 학생들에 대해서 관심을 가지고 지켜보고 계셨고 속으로 많이 응원도 하고 계셨던 것 같다. 좋은 강사님 덕분에 정말 수많은 개념들이 머리속에 제대로 정리되어 저장될 수 있었고, 그것들을 활용하여 프로젝트를 무사히 마무리 할 수 있었다. 정말 감사했습니다!

 

2. 새롭게 배운점(Learned)

애자일 방법론

  • 스프링에 대해 나름(?) 깊게 배울 수 있었던 것 만큼이나 이번 교육과정에서 만족스러웠던 것은 '애자일 방법론'에 대한 학습과 실제로 프로젝트에서 적용해본 경험이었다. 이전에 어디서 주워들은 것은 많아서 '애자일이 좋다', '좋은 기업들은 다 애자일한다'는 말에 무작정 애자일에 대해 찾아보고 학습해본 적이 있었다. 하지만 그렇게 배운 지식과 정보들은 너무 뜬구름 잡는 이야기들이었다. 결국 당시에 애자일은 포기하고 개발 공부나 열심히 하자며 씁쓸하게 돌아섰던 기억이 있다.
  • 하지만 이번에 초빙된 강사님은 정말 디자인 씽킹과 애자일 방법론에 도가 트신 분 같았다. 본인은 스스로를 '퍼실리테이터'라고 칭하셨던 것 같은데 강의 방식이나 내용이 매우 신선했고, 주입식 교육에 찌들어 있던 나에게는 다소 충격적이기까지 했다. 온갖 활동과 수많은 회의를 통해 우리는 디자인 씽킹과 애자일 방법론에 대해 배우고 실제로 경험해볼 수 있었다. 애자일이란게 단기간에 익히고 쉽게 적용할 수 있는 것은 절대 아닐 것 같다는 생각이 들었다. 이러한 방법론을 적용하고 도입하기 위해서 여러 회사들과 그 구성원들은 얼마나 노력했을지,,, 
  • 우리도 결국 프로젝트 초반에 애자일하게 가기 위해서 얼마나 많은 노력을 했으며, 얼마나 많은 회의를 가졌는지 모른다. 가끔은 애자일에 너무 목을 메는게 아닌가 싶을 정도로 개발을 미뤄두면서까지 회의를 진행했다. 사실 이때가 전체 프로젝트 기간을 통틀어서 가장 힘들었다. 팀원들의 불만도 하나 둘씩 등장하기 시작했다.  하지만 (다소 길 때도 있었지만) 반복적인 회의를 거치면서 생각보다 우리가 해야할 부분이 명확해졌고, 결과적으로 이는 프로젝트가 진행되는데 굉장히 긍정적인 영향을 미쳤다. 여러 의미에서 팀장님도 고생했고, 스크럼 마스터님도 고생했고, 열심히 따라온 팀원들도 모두 고생했다.!!

스프링 부트

  • 스프링과 스프링 부트에 대해서는 이전 학습일지에서 많이 다루었으므로 자세한 내용은 생략하겠다.
  • 하지만 개인적으로 미니 프로젝트와 파이널 프로젝트에서 서로 다른 스프링 부트 버전과 기술 스택을 사용해볼 수 있어서 무척 좋았다. 미니 프로젝트에서는 Spring Boot 2와 Mybatis, JSP를 주로 사용했고 파이널 프로젝트에서는 spring boot 3과 JPA(hibernate), Thymeleaf를 주로 사용했다. 두 프로젝트에서 서로 기술 스택이 다르다보니 개인적으로 학습하는 방법과 프로젝트에 적용하는 방법에서 차이가 있었다. 미니 프로젝트 때에는 기존에 수업시간에 한 번 깊이 학습했던 내용을 복습하면서 프로젝트에 적용하는 방식이라 조금 더 복잡하고 난이도있는 기능 구현에 도전해볼 수 있었다. 하지만 파이널 프로젝트 때에는 완전히 새로운 기술을 바로바로 배우면서 프로젝트에 적용해야 했다. 그렇기 때문에 한번에 복잡한 기능을 구현하려고 하기보다는 간단한 기능들(혹은 이미 구현된 기능들)을 조금씩 차근차근히 적용해보는 방식으로 접근했다. 

3. 부족했던 부분(Lacked)

커뮤니케이션

  • 사실 해당 과정에 들어오기 전에 개인적으로 세웠던 목표가 하나 있었다. 가능한 한 많은 사람들(= 될 수 있으면 모든 사람들)과 개발 혹은 진로와 관련된 주제로 이야기 해보는 것이었다. 과정이 끝난 지금 이 시점에서 해당 목표를 돌이켜봤을 때, 한 70% 정도는 달성한 것 같다. 다소 내성적인 성격 때문에 많은 사람들과 깊은 이야기를 나누는 게 쉽지만은 않았다. 그래도 누군가와 접점이 생겼을 때는 최선을 다해서 이야기해보려고 노력했던 것 같다! 
  • 그리고 사실 주변 가까운 사람들에게도 더 하고 싶은 말들이 많았지만, '관점의 차이' 혹은 '표현 방식의 문제'로 꾹 참았던 적이 많았다. 이럴 때마다 내가 조금 더 적극적으로 표현할 수 있는 사람이었다면, 그리고 상대방의 기분을 상하지 않게 재치있게 말할 수 있는 사람이었다면 더 좋았을텐데....하는 아쉬움이 남았다.

기술에 대한 이해

  • 위에서 말했듯이 새로운 기술을 바로바로 학습하여 프로젝트에 적용했을 때는, 완벽히 이해하지 못했지만 일단 구현을 위해 그냥 가져다가 쓴 코드들이 상당히 많았다. 프로젝트 기간이 좀 더 길었다면 분명히 팀원들끼리 스터디도 하고 토이 프로젝트도 진행하면서 기술에 대한 이해도를 높였을 것이지만, 현실적으로 그럴만한 시간이 부족했다. 여전히 물음표로 남아있는 기술들에 대해서는 꼭, 반드시 다시 학습하고 정리할 필요가 있다.

4. 앞으로 할 것(Longed for)

  • 프로젝트 리팩토링⭐
  • 스프링 공부 (+ CS 공부) 
  • 동료들과 꾸준히 연락하기
  • 알고리즘 다시 시작
Don't beat yourself up
Don't need to run so fast
Sometimes we come last
but we did our best
🐰🦊

이미지 - 스타터스 취업 부트캠프(STARTERS) 공식 블로그

1. 좋았던 점(Liked)

  • 이전에 JPA를 배워본 적도 사용해본 적도 없어서 막연한 두려움을 가지고 있었다. 하지만 스터디와 멘토링을 통해 JPA에 대해 학습하였고 실제로 프로젝트에 적용해보면서 감을 잡을 수 있었다. 초기에 엔티티를 제대로 정의하기만 하면 간단한 쿼리문을 작성하고 날리는 것은 정말 쉬웠다. 하지만 더 복잡한 쿼리문과 테이블 관계에 적용하는 것은 쉽지 않기 때문에 더 많은 공부가 필요하다.

2. 새롭게 배운점(Learned)

  • Spring Security와 OAuth2를 이용하여 소셜 로그인 기능을 구현해보았다. Spring Security와 Filter 또한 이전에 학습해본 경험이 없었기 때문에 다소 어려웠지만, 여러 서적과 블로그 글, 깃헙 소스코드를 참고하여 나름대로 이해해서 코드를 작성했다. 

3. 부족했던 부분(Lacked)

  • Spring Security 개념이 생각보다 방대해서 이해하고 프로젝트에 적용하는데 상당한 어려움을 겪었다. 사실 아직도 Spring Security와 Authentication, Authorization, Filter, Interceptor 같은 것들에 대해서 완벽히 이해를 하지 못했다.

4. 앞으로 할 것(Longed for)

  • Spring Security에 대해 천천히 학습을 한 후 다시 정리해보는 시간이 필요하다. 
  • Admin 페이지 고도화 작업에 참여할 예정이다.

이미지 - 스타터스 취업 부트캠프(STARTERS) 공식 블로그

1. 좋았던 점(Liked)

  1차 스프린트가 끝나고 2차 스프린트에 돌입했다. 1차 스프린트에는 회의가 잦아서 자꾸 정체되는 느낌이 들었는데, 2차 스프린트에 들어가면서 본격적인 개발을 시작하고 프로젝트에 속도가 붙기 시작했다. 실제로 1차 스프린트 기간의 대부분을 회의와 의사결정에 할애했는데, 그 당시에는 다른 팀보다 개발이 뒤처지지 않을까 매우 불안했다. 하지만 막상 2차 스프린트 기간에 들어가니 이전에 오랫동안 논의한 내용들을 바탕으로 개발이 생각보다 빠르게 진행되었고 눈에 보이는 결과물이 하나 둘씩 나타나기 시작했다. 물론 이전에 결정됐던 내용들을 다시 끄집어내어 다시 논의하는 경우도 간혹 있었지만 (매우 피곤하고 소모적이었지만 어쩔 수 없었다..), 팀장님의 의도대로 1차 스프린트에서 회의와 의사결정에 많은 시간을 할애했던 것이 결국 긍정적인 요인으로 작용했던 것 같다. 그렇지만 더 이상의 마라톤 회의는 사양한다. 다들 많이 지쳤다.

  그리고 지금까지 멘토님과 총 3번의 멘토링을 진행했다. 우리 조의 멘토님은 국내 유명 쇼핑몰 솔루션 업체에서 프론트엔드로 재직중이셨다. 13년이라는 어마어마한 경력을 가지고 계셔서 그런지 프론트엔드 뿐만 아니라 백엔드 관련해서도 상당히 깊이 알고 계셨다. 우리는 프로젝트 방향성 그리고 진행과정, 기술스택 등 프로젝트를 기획하고 진행하면서 갖게된 모든 궁금증들을 여쭤보았고 멘토님께서는 상세하게 답변해주셨다. 프로젝트를 진행함에 있어서 든든한 지원군이 생긴 것 같아서 매우 만족스러웠다. 뿐만 아니라 해당 교육 과정에 국한되지 않고 개발자 커리어 전체를 놓고 보았을 때에도 도움이 될만한 조언들도 많이 해주셨는데 그러한 점도 굉장히 마음에 들었다. 

2. 새롭게 배운점(Learned)

  JPA와 Thymeleaf에 대해서 필요한 지식만 빠르게 속성으로 학습하였고 그것을 프로젝트에 적용해보았다. 생각보다 간단하고 편리하게 쓸 수 있는 JPA 기능들을 복잡하게 사용하고 있었다. (이 부분은 멘토님의 조언으로 바로 잡을 수 있었다. 감사합니다 멘토님!) 실제로 JPA는 Repository나 Service 단에서 메서드를 호출하여 쿼리를 날리는 부분보다 Entity(Domain)와 Dto 단에서 테이블 정의를 제대로 하는 것이 훨씬 중요했다. 팀원 대부분이 JPA 경험이 많지 않았기 때문에, 우선 엔티티를 정의해서 테이블을 만들고 거기에 dummy data를 넣는 것을 가장 최우선순위로 잡았다. 하지만 일단 엔티티를 급하게 정의하고 테이블을 생성하느라 실제 로직에 적용했을 때 '순환참조' 문제가 발생했다. 이를 해결하느라 몇 시간의 구글링을 거치고 온갖 논의가 오갔지만, 결국 우리는 해결해냈고 정말 뿌듯했다. (해결한 것에 만족하지 않고 꼭 시간을 내어서 문제의 원인을 분석하고 해결 과정을 정리하자!)

 

3. 부족했던 부분(Lacked)

  개인적으로 며칠 동안 진행된 페어 프로그래밍에 익숙해져 있어서, 막상 혼자 개발을 진행하고 기능을 구현하려 하니 좀처럼 효율이 나지 않았다.  앞으로는 정신차리고 집중해서 내게 할당된 업무를 빠르고 신속하게 처리해야 겠다.

  그리고 우리의 목표였던 RESTful API를 완성시키기 위해서는 AJAX 혹은 Axios API, Fetch API가 필수적인 것 같았다. (Thymeleaf 하나 쓴다고 쉽게 해결될 문제가 아니었다..) 현 상황에서는 이에 대한 학습과 실습이 시급한 것 같다.

  마지막으로 지금까지 수많은 마라톤 회의를 해왔지만 정작 중요한 메서드 네이밍에 대한 논의는 하지 못했다. 그러한 상황에서 막상 개발에 돌입하니 서로 짠 코드를 정확히 이해하지 못하고 협업을 하게 되는 상황이 발생했다. 빠른 시일 내에 이 문제를 바로 잡아야 한다.

4. 앞으로 할 것(Longed for)

  1. Javascript 라이브러리에 대한 학습과 적용 필요
  2. Chat과 Login / Signup 기능에 대한 논의 및 구현

 

사진: Unsplash 의 Rod Long

When life gives you lemons, make lemonade

* 유데미 바로가기 : https://bit.ly/3V220ri

* STARTERS 취업 부트캠프 공식 블로그 보러가기 : https://blog.naver.com/udemy-wjtb

본 후기는 유데미-웅진씽크빅 취업 부트캠프 3기 백엔드 과정 학습 일지 리뷰로 작성되었습니다.

이미지 - 스타터스 취업 부트캠프(STARTERS) 공식 블로그

1. 좋았던 점(Liked)

 디자인 씽킹, 에자일 방법론, 스크럼에 대해서 3일 동안 특강을 들었다. 평소 '어떻게 하면 더 나은 방법으로 (효율적으로) 개발을 할 수 있을까?'에 관한 고민이 많이 있었고 나름대로 구글링해서 얻은 정보를 가지고 여러 시도를 해보기도 했다. 예를 들어 지난 프로젝트에서 일정과 목표를 조금 더 효율적으로 설정하고 달성률을 확인하기 위해 Zenhub이라는 툴을 사용했었다.  당시 Zenhub에도 Product Backlog, Sprint Backlog와 같은 항목들이 존재했지만 에자일에 대해 제대로 이해하고 있지 못해서 이와 같은 항목들을 제대로 사용하지 못했다. 이번 주 3일이라는 짧은 기간동안 나름 에자일 프로세스를 학습하고 이해하고 직접 경험해보았으므로 파이널 프로젝트 때 최대한 따라해보면서 체화하고 내 것으로 만들어봐야 겠다.

 

 

2. 새롭게 배운점(Learned)

디자인 씽킹(Design Thinking)

: 사용자에 대한 공감과 이해를 바탕으로 문제를 정의하고, 다양한 분야의 사람들과의 협업을 통해 창의적인 해결방안을 도출해내며, 신속한 프로토타이핑과 반복적인 테스트를 통해 결과물을 만들어내는 전략이다. 대부분 '공감-문제정의-아이디어-프로토타입-검증'의 프로세스를 따른다.1)

 

에자일 방법론

: 전통적인 폭포수 방법론에서 벗어나 조금 더 유연한 프로세스로 개발하는 방법론이다. 기능 단위의 프로토타입을 기반으로 일을 하기 때문에, 프로토타입 개발이 완료될 때마다 릴리즈하고 피드백을 받고 빠르게 수정을 한다.(이러한 과정을 반복적으로 수행한다.) 에자일 방법론을 실행하는 대표적인 도구로 '스크럼'이 있는데, 스크럼은 Sprint를 기반으로 실행한다. 2)

 

스크럼

:  Sprint라는 반복적인 개발주기를 지정해서 애자일 방법을 실행한다.2)

 

프로덕트 백로그

: 사용자(=페르소나)를 정의하고 조사하여 구현해야 할 기능들을 정의한 문서다. 모든 기능들을 특정 형식에 맞춰서 줄글로 표현해야 하며, 우선순위에 따라 정렬한다. Product Owner가 작성하며 하나의 스프린트가 끝나면 해당 문서를 업데이트하여 스프린트 회의에서 제시하여야 한다.3)

스프린트 백로그

: 요구 사항(=피드백)을 태스크로 구체화한 문서이다. 원칙적으로 수정이 불가능하며 테스트 주도 개발 계획이 포함되어야 한다. 스프린트 백로그는 Sprint 회의 때 결정된다.3)  

 

3. 부족했던 부분(Lacked)

 이번 주는 프로젝트를 본격적으로 시작하는 주였기 때문에, 주제 선정과 기능 정의를 위해 며칠동안 마라톤 회의를 진행했다. 따라서 목표했던 JPA 강의를 충분히 수강하지 못했다. 토요일에도 용산 교육장에 나가서 산출물 작성에 필요한 작업과 분담한 업무를 하느라 계획했던 공부를 하지 못했다. 필요하면 잠을 좀 줄여서라도 학습에 조금 더 집중해야 겠다!

 + 그리고 틀려도 상관없으니까 내가 이해한 것을 나만의 단어, 문장으로 설명하는 습관을 기르자. 우리 팀장님은 그런 것을 굉장히 잘 하시는데...보고 배우자!

4. 앞으로 할 것(Longed for)

  • JPA 강의 완강
  • 메인 페이지 View 만들기
  • 구현 기능 역할 분담 -> 개발 시작!

 

참고

  1. 디자인 씽킹과 새로운 미래, 사람인
  2. 에자일 방법론 - 스크럼 vs 칸반 데이터팀은?, 티스토리 블로그
  3. 애자일 스크럼 정리1(스크럼 개념/백로그/번다운 차트), 티스토리 블로그

 

 


* 유데미 바로가기 : https://bit.ly/3V220ri

* STARTERS 취업 부트캠프 공식 블로그 보러가기 : https://blog.naver.com/udemy-wjtb

본 후기는 유데미-웅진씽크빅 취업 부트캠프 3기 백엔드 과정 학습 일지 리뷰로 작성되었습니다.

이미지 - 스타터스 취업 부트캠프(STARTERS) 공식 블로그

1. 좋았던 점(Liked)

  미니 프로젝트가 끝나고 파이널 프로젝트를 위한 새로운 조가 편성되었다. 이전 조원들과 미니 프로젝트가 끝난 뒤 마지막 회고 겸 리뷰 회의를 진행하자고 약속했었고, 감사하게도 다들 바빴지만 짬을 내어서 참여해주셨다. 프로젝트를 하면서 느꼈던 점, 어려웠던 점, 미처 신경쓰지 못했던 부분들을 서로 공유했고 각자 부족하다고 느꼈던 부분이 채워지는 기분이었다. 정말 안 했으면 크게 후회할 뻔 했다.

  이번주 월요일부터 수요일까지는 다른 외부 강사님이 오셔서 '클라우드와 AWS(Amazon Web Services)'에 대한 특강을 진행했다. 이전에 AWS를 사용하여 웹 어플리케이션을 배포한 경험은 있지만, 책과 구글링을 통해서 급하게 학습한 뒤에 부랴부랴 배포를 진행해서 개인적으로 아쉬움이 많이 남았다. 마음 속에 항상 '시간만 있다면 AWS에 대해서 한번 제대로 배워봐야지'라는 생각을 가지고 있었는데 운이 좋게도 (짧은 기간이었지만) 이번에  AWS 전반에 대해 배울 수 있었다.

2. 새롭게 배운점(Learned)

  미니 프로젝트 회고를 진행하면서 프로젝트에서 내가 직접 구현하지 않았던 Filter, Validation, JavaMailSender의 개념과 동작 원리에 대해 자세한 설명을 들을 수 있었다. 이러한 개념들을 조금 더 찾아보고 학습하여 다음 프로젝트 때에는 내가 직접 구현하고 적용해볼 수 있었으면 좋겠다.

  수업시간에는 클라우드와 AWS에 대해 많은 내용을 학습했다.

클라우드

막대한 양의 IT 리소스를 온디멘드(on-demand) 방식으로 인터넷을 통해 제공하고 사용한 만큼의 비용을 지불하는 방식이다. 

...


3. 부족했던 부분(Lacked)

  AWS Lambda와 Jenkins에 대해서 이전부터 궁금했는데 이번 기회에 해당 서비스들에 대해 살짝 맛을 볼 수 있었다. 하지만 Lambda는 너무 짧게 학습하고 지나쳐서 어떤 기능을 구현하기 위해 어떤 방식으로 쓰는 것이 가장 좋은 방법일지 깊이 고민해보지 못했다. 

 

4. 앞으로 할 것(Longed for)

  미니 프로젝트 결과물을 처음부터 끝까지 (구조와 절차에 대해 신경쓰고 천천히 곱씹어보면서) 혼자서 구현해볼 것이다. 현상황에서 다른 무엇을 추가로 학습하는 것보다 많은 도움이 될 것 같다.

  그리고 아무래도 새로운 팀에서는 JPA 또는 Thymeleaf와 같은 새로운 기술 스택을 도입할 것으로 보이는데, 새롭게 배워야 하고 새롭게 적용해야 하는 것이라고 너무 부담감이나 거부감을 가질 필요는 없을 것 같다. 모르면 모르는대로 최선을 다해서 구현을 하면 되는 것이고, 추가적인 도움이 필요하면 같이 학습하는 동료들에게 양해를 구하고 물어보면 된다. (그러라고 동료가 있는 것이니까...!) 너무 자기 자신을 과신해서 무리하면 안 되지만, 그렇다고 지금처럼 너무 소극적으로 참여할 필요도 없는 것 같다!

 

 


* 유데미 바로가기 : https://bit.ly/3V220ri

* STARTERS 취업 부트캠프 공식 블로그 보러가기 : https://blog.naver.com/udemy-wjtb

본 후기는 유데미-웅진씽크빅 취업 부트캠프 3기 백엔드 과정 학습 일지 리뷰로 작성되었습니다.

이미지 - 스타터스 취업 부트캠프(STARTERS) 공식 블로그

1. 좋았던 점(Liked)

   이번주에는 미니 프로젝트를 진행했다. 본격적인 파이널 프로젝트에 들어가기 앞서 이제까지 배운 내용들을 적용해볼 수 있는 좋은 기회였다. 3일 동안 진행되는 간단한 미니 프로젝트였기 때문에 조는 가까운 자리에 앉은 사람들끼리 편성되었다. 운이 좋게도 리더십있는 팀장님과 열정적이고 적극적인 팀원들로 조가 짜여서 매우 수월하고 즐겁게 프로젝트를 진행할 수 있었다. 현실적으로 어렵겠지만.. 파이널 프로젝트도 이분들과 함께 하게 된다면 정말 즐겁게 할 수 있을 것 같다. 우리가 정한 프로젝트 주제는 ''간단한 개발자 질의응답 커뮤니티'였으며 서비스명은 '배.용.남(배워서 용기있게 남 주자)'이었다. 

배용남 메인화면
배용남 프로젝트 구조

  수업시간에 배운 내용과 예제들을 활용하여 우리 서비스에 맞게 DTO, DAO, Controller, Service, Filter 등을 구성하고 코드를 작성하여 서로 연결시켰다. DAO, DTO, Controller, Service는 테이블(User, Board, Comment) 별로 분리하였고 View단에서는 JSP와 JSTL, Bootstrap 라이브러리를 사용하여 화면을 만들었다. 프로젝트 규모가 크지 않고 기능들이 그렇게 많지 않았기 때문에, 화면별로 각자 역할을 분담했다. 

  혼자서 프로젝트를 세팅하고 구조를 설계하고 코드를 작성했다면, (나도 처음이기 때문에) 잘못 설계하고 잘못 작성하여 그것을 바로잡느라 상당한 시간이 걸렸을 것이다. 하지만 팀원들과 서로 의견을 나누고 조금 더 효과적인 방법이 무엇인지 고민해보는 시간이 있었기 때문에 비교적 수월하게 설계하고 코드를 작성할 수 있었다. 

2. 새롭게 배운점(Learned)

  실제로 Controller 로직을 구현하고 URL을 매핑하여 API를 만들다보니 한 가지 드는 의문이 있었다. GET으로 어떠한 URL을 호출할 때 특정 인자값을 전달할 경우, Query Parameter(board/detail?seq=23)로 줄 수도 있고 Path Variable(board/detail/23)로 줄 수도 있다. 수업 시간에는 강사님 코드를 따라치느라 바빠서 주의깊게 보지 못했던 부분이었는데 이번 미니 프로젝트 기회에 나름대로 다시 한 번 정리해보고 적용 방법을 익힐 수 있었다.

 

Query Parameter - Controller (@RequestParam 어노테이션 사용)

@GetMapping("/board/detail")
    public ModelAndView board(@RequestParam(value="seq", required=true) int seq, HttpSession session) {
        ModelAndView mv= new ModelAndView();
     
     	...
        
        return mv;
    }

Query Parameter - View(index.jsp)

<c:forEach items="${boardList }" var="board">
<tr onclick="location.href='board/detail?seq=${board.boardSeq}'" id="ajaxtr">

 

Path Variable - Controller (@PathVariable 어노테이션 사용)

@GetMapping("/board/updateboard/{seq}")
    public ModelAndView updateBoard(@PathVariable("seq") int seq) {
        ModelAndView mv = new ModelAndView();
        
        ...
        
        return mv;
    }

Path Variable - View(detail.jsp)

<button type="button" onclick="location.href='updateboard/${board.boardSeq}'">수정하기</button>

3. 부족했던 부분(Lacked) 

  아무리 구현해야 하는 기능이 적었다고 하지만, 현실적으로 3일을 온전히 로직 작성에 사용할 수는 없었다. 초기 개발 환경 세팅과 깃 컨벤션 설정 그리고 협업 과정에서 발생하는 여러 에러와 충돌을 해결하는데 시간을 상당히 소비했기 때문이다. 돌이켜보면 '클린 코드'와 'RESTful한 API 설계 규칙'(그런데 JSP만으로 뷰단을 구성하는데도 RESTful한 설계가 필요한가...?? 한번 알아봐야 겠다..)에 대한 고려를 많이 하지 못한 것 같다. 분명 여러 메서드 사이에 중복된 로직이 존재했고 이에 대해서 팀원들과 논의해보고 해결방법을 찾아보고 싶었지만 시간적으로 여유가 없었다. 그리고 API 설계 규칙에 대해서도 충분히 논의를 하지 못해 팀원별로 API가 약간씩 상이하게 설계되고 작성되었다는 사실을 마지막 날에야 뒤늦게 파악할 수 있었다. 

 

4. 앞으로 할 것(Longed for)

 이후에 (가능하면) 팀원분들과 프로젝트를 회고하는 시간을 가지면서, 잘했던 부분들과 부족했던 부분들에 대해 의견을 나눠보고 개선할 부분이 있으면 개선을 해보면 좋을 것 같다.

 

#. DDC`23 개발자 컨퍼런스 요약

  1. 이번주 토요일(23.01.28)에 삼성역 코엑스에서 열렸던 멋쟁이사자처럼 주관 개발자 컨퍼런스다. (약 800명 참석)
  2. '성장'과 '조직 문화' 등과 관련하여 현업 개발자들(무신사, 뱅크샐러드, 강남언니, AWS, 위니브, 마인딩, 토스)의  가치관과 경험을 들을 수 있는 기회였다.
  3. 개인적으로 발표 내용은 뱅크샐러드 개발자 분과 위니브 대표님이 가장 좋았다.
  4. 솔직히 발표 세션보다 마지막의 패널 토크 세션이 훨씬 재밌고 유익했다.(딱딱하지 않은 분위기에 개발자 분들의 속마음과 경험, 조언등이 더 잘 공유되었던 것 같다)
  5. (발표 내용과는 별개로) 강남언니 개발자 분이 발표를 가장 자연스럽고 프로페셔널하게 잘 하셨다.(진짜 부럽다...)
  6. AWS는 혹시나 했는데 역시나....였다. 기대한 내가 바보
  7. 원래 안 갈까 고민했는데, 안 갔으면 정말 후회할 뻔 했다.
  8. 이런 기회들이 또 있으면 주변 사람들과 적극적으로 공유해서 더 많은 사람들과 함께 가고 싶다.
  9. 사실 이날의 메인 일정은 '고수가 잔뜩 올라간 멕시칸 타코 먹기'였다. ("비야게레로")
  10. 타코맛은 말할 것도 없고, 같이 간 일행분들이 만족해하셔서 너무 행복했다. 

* 유데미 바로가기 : https://bit.ly/3V220ri

* STARTERS 취업 부트캠프 공식 블로그 보러가기 : https://blog.naver.com/udemy-wjtb

본 후기는 유데미-웅진씽크빅 취업 부트캠프 3기 백엔드 과정 학습 일지 리뷰로 작성되었습니다.

+ Recent posts