개괄

처음으로 진행하는 협업, FE와 BE 모두 협업은 처음이었다.

대박 프로젝트의 꿈을 안고 협업을 시작하기로 한 대현씨와 영준씨.

그들은 git repo에 프로젝트명을 넣고 그 안에 모든 FE/BE의 코드를 몽땅 넣어버린다.

 

문제 발생

결과

그 결과 미친 키메라가 탄생했다.

한 프로젝트 안에 fe/be를 모두 넣고 돌릴때 두명 다 터미널에서 npm start를 하고, 스프링 컨테이너를 띄웠다.

프론트와 백 전부다 gitignore에 코드를 넣었다.

리포지토리는 난잡해졌고 이게 뭔지 모르겠는 상태가 되어버렸다.

그러다 프론트는 SOS를 요청하러 갔고 백엔드는 다른 사람들의 프로젝트를 염탐하러 갔다.

그리고 발견한 다른 팀의 리포지토리. 이게 뭐야!

이렇게 깔끔한 repo가 있을 수가 없다. 그때서야 github Organization에 대한 필요성을 알게 되었다.

그 전까지는 Organization이란 그냥 "나도 pr 받게 해줘"의 연장선인 줄만 알았는데..

그제서야 organization을 만들고 그 안에 서로의 repo를 파서 코드를 나눠 담았다.

이렇게 깔끔할 수가 없다.

이게 협업의 기본이구나 라는걸 또 새삼 깨달아버렸다.

 

알게 된 점

한 레포에서 BE와 FE를 모두 나누어 담는것은, 1인 프로젝트 때나 하는 것이고,

다른 사람과의, 특히 다른 단에 있는 개발자와 협업을 할 때는 레포지토리를 꼭 나눠야 하는구나 라는것을 깨달았다.

'GIT' 카테고리의 다른 글

Conflict의 냄새가 날 때  (1) 2025.01.16
TIL - 아무것도 못 본척 해야 돼! "REVERT"  (0) 2023.02.06

개괄

3주차를 시작하기에 앞서서 다음과 같은 과제의 제출을 요구받았다.

첫 번째는 BUS 클래스를 모델링하고, 두번째는 Requirements를 만족하는 BUS 클래스를 리팩터링 하는 것이다.

문제 발생

문제를 해결하는것은 그렇게 어렵지 않지만, 요구사항을 맞추면서 미래의 요구사항을 생각해서 만드는 '설계'가 쉽지 않았다. BUS 클래스는 어떻게 만들어야 할까? 그리고 BUS객체를 생성해주는 팩토리 메서드는 어떻게 생성해야 할까?

https://github.com/choincnp/Hanghae_4thweek

 

GitHub - choincnp/Hanghae_4thweek

Contribute to choincnp/Hanghae_4thweek development by creating an account on GitHub.

github.com

 

시도해본 것들

팩토리 메서드 패턴이란?

"구체적으로 어떤 인스턴스를 만들지는 서브 클래스가 정한다."

다양한 구현체(Product)가 있고, 그 중에서 특정한 구현체를 만들 수 있는 다양한 팩토리(Creator)를 제공할 수 있다.

OCP와 비슷한 맥락이다. 최대한 메서드를 팩토리 인터페이스에 빼놓고 구체적인 것만 하위 모듈에서 정한다.

핵심 : 새로운 클래스를 만들 때 기존 코드를 건드리냐 아니냐가 핵심

복습 :

팩토리 메소드 패턴을 적용했을때의 장점, 단점

  • 장점
    • OCP를 잘 지킬 수 있다. 객체가 추가되어도 if-else등의 구문에서 자유롭다.
    • 코드를 간결하게 구현할 수 있다.
  • 단점
    • 클래스가 너무 많이 늘어난다.

여기서 OCP란 무엇일까?

  • SRP와 마찬가지로 기존 클래스에서 어떤 클래스를 수정해야 할 때 기존 클래스를 건드리지 않고 새로운 인스턴스를 다른 방법으로 확장할 수 있어야 한다.
  • 새로운 클래스를 설계할때는 확장성이 있어야 한다.
  • 나중에 설명할 때 “무엇”이 확장이고 “무엇”이 변경인지 말해줘야 함.

DIAGRAM

설계한 것을의 Diagram을 뽑아내면 다음과 같다.

원래는 자바 11 위에서는 interface 안에 default modifier를 사용해 구현된 메서드를 넣을 수 있지만,

내가 아직 배우기로는 "인터페이스 안 모든 메서드는 public abstract여야 한다"이기 때문에, 팩토리 인터페이스 안에 모든것을 넣기보다는, 하위의 busfactory라는 추상 클래스에 의존하도록 설정했다.

나중에 혹시나 대형버스 / 소형버스별로 다르게 생성할 수 있어야 해서

그 busfactory를 상속받는 LargeBusFactory와 SmallBusFactory로 나눠 구현했다.

Bus 인스턴스와 Taxi 인스턴스는 Vehicle을 상속받으며, 위의 요구사항을 맞춰 설계했다.

버스의 상태는 Enum으로 운행중과 차고지행으로 나누었다.

 

가장 고민한 부분은, 요구사항 중 버스 객체는 고유값으로 설정되어야 하는데, 큰 버스와 작은 버스의 고유값은 겹치지 않게끔 해야하는 것이었다.

 

그래서

BusFactory 안에 busNum을 설정해두고,

BusFactory를 상속받는 하위 XXXBusFactory에 sequence를 설정해 두었다.

사실 이렇게 쓰고보니 꽤 간단한 일이었는데, 당시에는 정말 고민을 많이 했다.

 

팩토리 메서드의 단점으로 많은 클래스를 꼽았는데, 디렉토리로 나눠두면 오히려 더 깔끔하고 좋을 것 같다.

개괄

스프링 컨테이너는 자바에서만으로는 지키기 어려운 OCP와 DIP를 지킬 수 있게끔 도와준다.

하지만 OCP, DIP가 무엇인지 SOLID에서 깨닫지 못하면 말짱 도루묵이 된다.

또, SOLID를 이해하고 적용하기 위해선 상속과 인터페이스에 대한 이해가 있어야 한다.

상속과 인터페이스란 무엇일까?

 

목표

상속과 인터페이스를 알고, 설계의 핵심 원리에 다가갈 수 있게끔 한걸음 나아가보자.

 

배운 것 - TIL SERIES(LINK)

붕어빵 장사와 함께하는 클래스, 메서드, 추상 클래스와 인터페이스

 

 

팩토리 메서드 디자인 패턴과 함께하는 버스 공장 만들어보기

https://choincnp.tistory.com/44

 

TIL - 팩토리 메서드 디자인 패턴과 함께하는 버스 공장 만들어보기

개괄 3주차를 시작하기에 앞서서 다음과 같은 과제의 제출을 요구받았다. 첫 번째는 BUS 클래스를 모델링하고, 두번째는 Requirements를 만족하는 BUS 클래스를 리팩터링 하는 것이다. 문제 발생 문제

choincnp.tistory.com

 

목표 달성 여부

저번주와는 다르게, 이번주는 조금이나마 객체지향이 무엇인지를 알 수 있었다.

물론 개념은 디스크 조각 모음처럼 내 머리에 저장되고 있지만 이번주는 그 조각이 조금씩 연결되는 것 같아 기뻤다.

많은 것을 공부하지는 못했지만, 그래도 이렇게만 하면 언젠가는 발전할 수 있을것이라는 기대를 가졌다.

 

느낀 점

늘 그랬듯, 설계를 한다는 것은 내 눈앞에 보이는 것이 아니라 숲을 미리 보고 작성하는 것이고,

더 나아가서 보이지 않는 미래를 보며 꿈꾸고 꾸미는 것이 설계이다.

그래서 좋은 설계는 추상적이어야 하고, 추상적인 것에 의지해야 한다.

'인간 군상'이라는 말이 있듯, 클라이언트는 언제 어떤 요구조건을 들이밀 지 모른다.

그것이 가능하든, 불가능하든 최대한 생각해서 프로그래밍을 해야 한다.

 

다음 주 목표

누구나 인강을 들으면 '아~이거구나'라고 이해할 수 있다. 이해를 하지 못해도, 이해를 해도 같은 반응이 나온다.

그런데 또 막상 이걸 적용하려고 하면 굉장히 힘들다.

그렇지만 세상은 쳐맞는 기회를 받기조차 어려운 것이 현실이다.

그러나 흙 속에도 한줄기 빛이 있으니, 프로젝트나 나만의 코드를 짜면서 적용시켜보면 정말 빠르고 쉽게(?) 이해가 된다.

이번주도 프로젝트나 과제에서 배운 내용을 많이 이해해보도록 해야겠다.

지뢰찾기류의 문제는 생각보다 여기저기 많다.

처음 이런 델타배열류의 문제를 본 것은 프로그래머스였다.

프로그래머스의 안전지대 문제를 풀었는데, 저 때는 델타 배열의 존재를 몰라서

import java.util.*;
class Solution {
    public int solution(int[][] board) {
        int answer = 0;
        int length = board.length;
        int[][] board2 = new int[length+2][length+2];
        for (int i=0; i<board2.length;i++){
            Arrays.fill(board2[i],0);
        }
        for (int i=0;i<length;i++){
            for (int j=0;j<length;j++){
                if (board[i][j]==1){
                    for (int k=-1;k<=1;k++){
                        for(int l=-1;l<=1;l++){
                            board2[i+k+1][j+l+1] += 1;
                        }
                    }
                }
            }
        }
        for (int i=1;i<=length;i++){
            for (int j=1;j<=length;j++){
                if (board2[i][j]==0) answer++;
            }
        }
        return answer;
    }
}

이런 긴 4중 for문으로 문제를 풀었었다. 물론 문제가 좀 어렵기도 했다.

 

다음은

다음도 이와 비슷한 봉우리 찾기 문제인데, 여기는 친절하게도 한칸씩 미리 늘려서 알려주었다.

public class Main {
    public int solution(int[][] arr) {
        int cnt = 0;
        for (int i=1; i<arr.length-1; i++){
            for (int j=1; j<arr.length-1;j++){
                if ((arr[i-1][j] < arr[i][j])
                        &&(arr[i+1][j]<arr[i][j])
                        &&(arr[i][j+1]<arr[i][j])
                        &&(arr[i][j-1]<arr[i][j])) cnt++;
            }
        }
        return cnt;
    }

문제가 그렇게 복잡하지는 않았는데, 만약 이게 전후좌우, 또는 전후좌우대각선, 또는 

이거 조건식 걸다간 죽어버릴거야

이런 류로 탐색을 해야 한다면 어떨까?

for문을 하루종일 돌리거나, if조건문에만 엄청나게 식을 많이 써서 돌릴 것이다.

이 때 필요한 것이 델타 배열이다.

델타 배열은 간단하다.

일단 먼저 상하좌우 4방향을 탐색해야한다고 생각해보자. 이 때,

int[] dx = {-1,0,0,1};
int[] dy = {0,-1,1,0};

를 만들어 i+dx = nx, i+dy = ny로 놓고 푼다면 내가 있는 좌표의 위치를 찾아볼 수 있다.

더 나아가서 대각선까지 포함하는 8방을 탐색해야한다고 생각해보자.

int[] dx = {-1, -1, -1, 0, 0, 1, 1, 1};
int[] dy = {-1, 1, 0, -1, 1, -1, 1, 0};

이 때 for문을 이용해 8번 돌리면 내 주위의 방향을 다 파악할 수 있다.

public char[][] solution(int n, char[][] arr){
        char[][] answer = new char[n][n];
        int[] dx = {-1, -1, -1, 0, 0, 1, 1, 1};
        int[] dy = {-1, 1, 0, -1, 1, -1, 1, 0};
        for (int i = 0; i < arr.length; i++) {
            for (int j = 0; j < arr.length; j++) {
                if (Character.isDigit(arr[i][j])) {
                    answer[i][j] = '*';
                } else {
                    int tmp = 0;
                    for (int k = 0; k < 8; k++) {
                        int nx = i + dx[k];
                        int ny = j + dy[k];
                        if (nx >= 0 && nx < arr.length && ny >= 0 && ny < arr.length) {
                            if (Character.isDigit(arr[nx][ny])) tmp += arr[nx][ny] - '0';
                        }
                    }
                    if (tmp > 10) answer[i][j] = 'M';
                    else answer[i][j] = (char) (tmp + '0');
                }
            }
        }
        return answer;
    }

이렇게 델타 배열을 이용하면, 조건문을 훨씬 편하게 짤 수도 있고,

또 혹시나 위에 그림처럼 8방이 아닌 많은 방향에서도 정확하게 탐색을 할 수 있다.

 

좋아하는 말 중에 E=MC^2라는 말이 있다.

Error = More Code라는 말인데, 델타 배열을 쓰지 않으면 너무 많은 코드로 더 많은 에러를 야기시킬 수 있다.

무엇이 틀린지 한참 고민해야하는것은 덤이다.

 

추가 팁)

1. 다른조건보다 늘 nx, ny의 유효성검사를 해주어야 한다. 그렇지 않으면 ArrayOutOfBoundException 에러가 난다.

2. 가장 안쪽에 for문을 돌 때 break를 걸어주면 더 편하다. 예를 들어 봉우리 문제인 경우, 내 주위 8방향보다 내가 값이 더 커야 하는데, 다른 방향에서의 value가 더 클 때 break를 걸어주면 최대 7번의 for문이 도는것을 절약할 수 있다.

많은 답이 있을 것 같은 소수 찾기 문제. 

내가 생각해본 나의 풀이 :

isPrime으로 각 소수를 찾는다( i=2일때부터 root n까지 탐색)하고 n%i == 0이면 false를 반환한다.

이 isPrime으로 n까지 수들 돌면서 소수를 뽑는다.

그러나 훨씬 빠른 방법으로 소수를 찾을 수 있다.

그냥 이렇게 써 놓으면 무슨 말인지 모를 수가 있다.

어떤 수부터 n이라는 수까지 탐색을 하면서

배열을 지워 나가는 것이다

1) i=2일 때

i번째 인덱스를 가지고 있는 친구가 처음으로 내용물이 0인 상태로 발견이 되었다.

그러면 answer를 하나 증가시켜주고, 2의 배수인 친구들을 전부 내용물을 1로 바꿔준다.

이건 for문안의 j=j+i에 설정되어있다.

2) i=4일 때

아까 i=2로 인해 내용물이 1로 변했으므로 지나간다.

 

2중 포문을 돌기 때문에 굉장히 느릴것이라고 생각했지만, 내가 짰던 코드보다 훨씬 빠르다.

특히나 for문 안에 있는 j=j+i 스킬이 굉장히 유용할 것 같다.

하나의 포문 안에 변수를 2개 돌리는 스킬도 좋으니 찾아보자

개괄

이제 겨우 포트 3000과 8080은 연결했다.

그런데 내가 만들어야 하는것은 FE에서 준 query를 가지고 youtube에 get 방식으로 요청을 보내서 받은 response를 파싱해서 다시 FE로 보내주어야 프론트에서 그것을 렌더링할 수 있고, 그 기능을 실제로 구현해야 한다!

youtube api를 쓰면 쉽게 파싱할 수 있지만, 화이트카드를 계속 발급받으면서 Auth를 하는데 번거로움이 있어 그냥 대가리 박고 파싱하기로 했다.

 

문제 발생

근데 이거 어케함?

8080에 요청이 들어올때 response를 쥐어주는 법, request로 파싱하는 법은 알았다.

근데 내가 클라이언트 입장에서 다른 서버에 요청을 보내고 받아오는 것은 한번도 해보지 않은 기술이라 벌벌 헤멨다.

어찌저찌 유튜브에서 받아온 html(유튜브는 SSR 방식을 이용하기 때문에, 쉽게 말하면 빈 템플릿을 보내고 그 안에 이런저런 요청받은 정보들을 가져와서 렌더링한다). 은 또 어떻게 파싱할꼬...

 

시도해본 것들

일단 처음으로 youtube에 get 메서드로 통신하기 위해서는 여러 방법이 있었다.

  • HttpUrlConnection 사용하기 -> 너무 옛날 기술이라 요즘엔 잘 사용하지 않는다고 함
  • RestTemplate -> Spring 3 이후에 나온 기술, 현재는 또 Depricated당했다.
  • WebClient -> Spring 5 이후에 나온 기술

그중에 RestTemplate를 사용해보기로 했다.

UriComponentsBuilder를 이용해 Uri에 쿼리를 담아 보냈는데,

텅 빈 ResponseBody만 온다..

다음 코드의 문제점은 뭘까?

8080포트에서는 200번대로 다 떴는데, status code를 로그로 찍어보니 300번대가 떴다.

왜 301이 떴지? 하면서 온갖 생각을 다 하다가 하나를 생각해냈다.

HTTPS

HTTPS 프록시 기술을 사용하기 때문에 리다이렉팅을 하는 과정에서 불러오지 못했던 것이었다.

이걸가지고 3~4시간을 헤메다니...

아무튼 성공했고, 다음과 같은 responseBody를 받았다.

전체는 이거의 한 6배정도 되는 것 같다.

이거를~ 어떻게~ 파싱할까요~?

이 긴 String 사이에 JSON이 숨어있다. 그 숨겨진 JSON을 꺼내서 우리가 원하는 타이틀로 바꿀것이다.

jsoup은 html parser라 html이 변동되면 사용할 수 없기 때문에

youtube api를 쓰지 않으려면 일일히 파싱을 해 주어야 한다. (CSR은 깡통 html만 주기 때문에 jsoup을 쓸 수 없다.)

파싱은 JSONParser를 이용해서 파싱하려고 했다.Json.simple 라이브러리를 이용해서 바꾸어 주려고 하니

이 안에 파싱을 할 때 JSONObject로 파싱되는지, JSONArray로 파싱되는지에 따라 계속 확인해주어야 하고, 계속 형변환을 해 주었다.

JSONParser를 쓰는 레퍼런스들은 많아서 찾아볼만했지만 보통은

[{곤충1 : 잠자리}, {곤충2 : 매미}] 등의 단순화된 스트링에서만 파싱을 했고,

{asdfasdfdsfa:{asdfsdafsdfa{asdfdsaffdsafds:gdfadsaffdsafsadfdsa:sdaffsadfsadfdas:···}···}···}등의 구조를 알아보기 힘든 긴 JSON을 파싱하는 레퍼런스들은 없었기에, 일일히 해 주어야 했는데,

이 문장을 1억줄 쓰려니 생각만 해도 아찔했다.

바로 ObjectMapper의 등장이다.

ObjectMapper에서 readTree로 데이터를 읽으면 JsonNode로 반환한다. 더욱이나 좋은것은, get 메서드 안에 index와 fieldname을 둘 다 혼용해서 쓸 수 있기에 형변환을 안해주어도 된다는 점이 매우매우 마음에 들었다.

 

알게 된 것

머리를 박으면서 코딩을 하다보니 알게된것들이 있었다.

1. BE api의 flow

MusicParsercontroller에서 MusicParserService를 const injection을 통해서 주입하고 GetMapping으로 /search에서 FE에서 날려준 request를 가지고 MusicparserService.method(request)한다는 것

2. RestTemplate의 흐름

resttemplate 구현 => getForObject로 json형식의 string값을 받고(getForEntity써서 entity로 결과 반환받아도된다.)
=> 그다음에 그걸 service단의 muisicparser로 가져가서 파싱하고 =>그다음에 프론트에 정보 줌

 

그럼 이제 파싱을 해서 프론트로 던져주는 작업을 해 보자.

개괄

알고리즘 주차도 정말 힘들지만, 개인적으로는 정말 존경하는 김영한 선생님이 말씀하셨던 정말 잘하는 주니어의 특징인 '야생형'이 그리워졌다. 마침 내가 정말 존경하기도 하고 가장 번뜩이는 아이디어를 가졌다고 생각하는 프론트엔드 친구가 프로젝트를 추천했고, 같이 하게 되었다.

거창한 프로젝트는 아니고 2명이서 하기에 사실 git flow나 개발 프로세스를 익히기엔 턱없이 부족하지만, 실전 프로젝트 대비를 하는 입장에서는 정말 좋다고 생각했다.

늘 어디서 받은 js/html로 렌더링을 하며 혼자 개발 공부를 해왔지만, 이번에 처음으로 다른 언어와 같이 소통하고 정보를 주고받는다는것이 나를 두근두근하게 만들었다.

 

문제 발생

혼자하는 개발은 나혼자 정보를 주고받지만, '협업'은 다른 '사람'과 정보를 주고받을 수 있어야 한다.

FE는 3000 포트를 가져가고, 스프링부트는 8080부트를 가져간다.

그 사이에서 정보를 주고받아야 하는데, 처음부터 갑자기 너무 막막해졌다.

"프로젝트 생성은 했지만, 어떻게 해야 정보를 주고받을 수 있을까?"

혼자 인강을 보고 혼자 개발을 하면 8080포트만 사용하면 되는데, 3000 포트와는 어떻게 통신하는지를 몰랐다.

기능 개발에 앞서 리액트와 스프링부트 연동 과정부터 진행해야 했는데, 해본적도 없고 지금은 협업 주차도 아니니

BDD(Blog-Driven-Development)를 해야 했다. 그러나 BDD에는 큰 문제가 있었는데, 각 블로그마다 문제는 같아도 해결하는 방식이 천차만별이었다.

 

시도해본 것들 ( 백엔드 입장) : CORS 오류가 뜰 것을 대비하고 작성함

1. 먼저 Controller를 만든다

RequestMapping, GetMapping은 실험하기 위해 일부러 다르게 설정해 보았다.

2. setProxy.js파일을 생성해본다

Origin 구성요소가 하나라도 다르면 CORS 에러가 발생한다고 해서 미들웨어 설치도 해 보았다.

3.app.tsx에 axios과 관련된 설정 import해주고, 

4. WebConfig 설정파일 만들기

이렇게 했더니 3000 포트에서 8080 포트의 내용을 받아올 수 있었다.

3000포트에서 "Hello MeoHyun"을 받아왔을 때의 그 쾌감은 처음에 Hello World를 쳤을때보다 쾌감이 30배 이상이었다.

그런데 문제가 생겼다.

나는 되는데 프론트단에서는 접속이 불가능했다.

찾아보니 이 에러는 IP주소가 비공개로 설정되어있을 경우 값을 할당받지 못해서 생기는 에러라고 한다.

생각해보니 나는 노트북에서 wi-fi 환경으로, FE는 랜으로 접속해서 오류가 터지는 것이었다.

그래서 root directory에 .env 파일을 생성해서 

DANGEROUSLY_DISABLE_HOST_CHECK=true

이 한줄만 넣어주니 말끔히 완료되었다.

나중에 다른 백엔드에 물어보니, package.json에 프록시 코드 하나만 추가해주면 된다고 하였다.

 

협업이라는게 그냥 같이 코드만 만지고 하는건 줄로만 알았는데, 생각보다 복잡했다. 그리고 배울게 너무나 많아졌다.

개발이 정말 재미있다. 

'웹개발' 카테고리의 다른 글

TIL - 스프링 프레임워크에 관한 간단 정리  (0) 2023.02.12

개괄

주특기 세션으로 들어가는 길목의, 어쩌면 가장 중요하면서도 가장 핵심적인 주

자바를 배우는 주간이었다.

개인적으로 Spring에서는 자바의 모든 문법들을 자유자재로 구사할수 있을 정도는 되어야 한다고 생각하는데,

그래서 자바에 대해서, 그리고 스프링을 하는 이유인 '객체지향'을 위해 얼마만큼 다가가느냐가 핵심이 되겠다.

 

목표

객체지향 프로그래밍이란 무엇인지 깨닫고 JVM은 왜 존재하는지, 그리고 JVM의 동작 원리에 대해 이해하고 CS지식은 왜 쌓아야 하는지를 알아야 한다.

 

배운 것 - TIL SERIES(LINK)

oop의 핵심요약

https://choincnp.tistory.com/28

 

TIL - OOP 요약

OOP 핵심정리 캡슐화 상속 추상화 다형성 OOP의 장점 유지보수 Easy 재사용성 ⬆️ 중복제거 Easy OOP 작성 순서 클래스 생성 (설계도 작성) 인스턴스 생성 (제품 생성) 인스턴스 사용 (제품 사용) 클래

choincnp.tistory.com

 

JVM이란 무엇인가요

https://choincnp.tistory.com/22

 

TIL - THE JAVA(1), JVM

개괄 IT 인프라 구조라는 책을 읽다가 문득 웹 서버와 AP 서버에 대한 이야기가 나왔다. 웹 서버의 OS 안에는 '프로세스'라는, 독립된 메모리 공간을 가진 것이 있다. AP서버에도 '스레드'라는 프로

choincnp.tistory.com

 

목표 달성 여부

객체 지향적 언어란 말 그대로 '지향'일 뿐이다. 객체 지향적 언어에 정답은 없다고 생각한다.

객체 지향에 다가가고 있어도, '이것이 객체 지향이다'는 말은 섣불리 할 수가 없었다.

SOLID 원칙을 배우면서 객체를 어떻게 구성해야 하는지에 대해서도 조금은 알 수 있었지만,

내가 앞으로 짜는 코드에 대해서 어떻게 객체지향적으로 짜야하는지는 아직 너무 이르기도 하고 디자인 패턴들을 많이 배워야 할 것 같다. JVM도 마찬가지다. CS지식이 전무한 내게 JVM의 시스템 구조를 조금 본 것은

혀에 잠깐 한 방울 닿은 정도로만 다가왔다.

그래서 이번주는 솔직히 점수를 많이 줄 수 없다고 생각한다.

조금 더 과장을 보태면 0점을 주고 싶다.

 

느낀 점

아는 것을 다시 공부하기는 굉장히 힘들다. 자바의 정석을 몇번이나 봐 온 나로써는 솔직히 조금 지루했다.

문제는 지루하면 안 된다는 것에 있다. 볼때마다 새로운 지식이 나오는데도 불구하고 무엇에 우선순위를 두어야 하는지에 대해 계속 뺑뺑 돌았다. 알고리즘 공부하다가 자바보다가 새로운 지식 보다가 하니 결국 남은게 없었다.

아는 형에게 정말 좋은 제안이 왔다. 스프링 개발을 부로, swift를 주로 하는 곳인데, 과장님이 나를 좋게 보신다고 이력서를 한 번 내보라고 했었는데, 그것에 대해 정말 고민했다.

그리고 나서 며칠 뒤 밤에, 같이 개발자를 목표로 했던 동료에게도 전화가 왔다. 면접을 봤었는데 스프링은 왜 쓰는지, 또 다른 개념에 대해선 무엇을 알고 있는지를 물어봤다고 했다. 곰곰히 생각해 봤는데 내가 거기에 대해서 답변할 수 있는게 너무 적었다. 그래서 부끄러워서 아는 형에게 조금 더 공부해야 할 것 같다고 연락을 남겼다.

이 상태로 취업을 했다간 나보다 나를 소개시켜준 형에게 부끄러워서 회사를 다니지 못할것 같았다. 취업시켜준다는 말은 안했지만... 정말 많은 것을 보고 듣고 알고 가야겠다고 생각했다.

 

다음 주 목표

하나에 몰두하지 않고 여러개를 돌린 시도 자체는 좋으나 결과가 너무 참담했다. 조금 더 개념에 치중해서 공부하고, 스프링 주간에 가면 할 게 많으니 빠트리지 말고 모조리 챙기고 가도록 하자. 

 

싱글톤 패턴이란?

인스턴스를 오직 하나만 제공하는 패턴을 말한다.

시스템 런타임, 환경 세팅에 관한 정보 등 인스턴스가 여러개일때 문제가 생길 수 있는 경우 인스턴스를 하나만 제공한다.

  • 마우스 포인터가 2개일경우 생기는 문제상황
  • 회원 정보를 관리하는 리포지토리
  • 주문 서비스 도우미 등

이럴 때 유의해야 할 사항은 어떤게 있을까?

  • new 연산자를 사용하지 못하게끔 Access Modifier를 private로 설정해 둔다.
  • Global Access를 하려면 getter, setter, 그리고 getInstance를 메서드로 둔다.
  • getInstance 메서드 안에도 new가 함부로 들어가면 안 되니 null check를 꼭 해둔다.

예제

영준이네 동아리 '프로토스' 는 서로 '칼라'라는 중앙 소통 장치로 소통을 한다.

그런 입장에서 칼라가 2개일 경우 서로 소통이 되지 않아 큰 문제가 발생한다. 그래서 칼라는 꼭 단일 개체로 유지해주어야 한다.

public class Protoss{
	public static void main(String[] args){
		Khala khala = Khala.getInstance()
		Khala khala1 = Khala.getInstance()
		System.out.println(khala==khala1)
	}
}

칼라와 칼라1이 같은 인스턴스임을 확인하려면 어떻게 해야 할까?

class Khala{
	private static Khala khala
	public static Khala getInstance(){
		khala = new Khala(); // 매번 new 연산자 돌기때문에 다중분신술됨
	}
}

먼저 이렇게 클래스를 설계해봤다. 여기서 문제가 발생한다.

=> getInstance를 실행할때마다 새로운 칼라 객체가 등장한다.

그래서 칼라 클래스를 다음과 같이 바꿨다.

class Khala{
	private static Khala khala
	public static Khala getInstance(){
		If (khala == null){
			khala = new Khala();
		}
		return khala;
	}
}

칼라 객체가 없을 때만 새로운 객체를 생성하고, 칼라 객체가 있을때는 기존 칼라 객체를 반환했다.

그런데 여기서 또 문제가 발생한다.

=> 우리는 보통 싱글쓰레드가 아닌 멀티쓰레드 환경에서 프로그램을 실행하기 때문에 쓰레드가 2개일 경우 다음과 같은 상황이 발생한다. 거의 속도차이가 없는 쓰레드1과 쓰레드2가 동시에 getInstance() 안으로 들어왔다고 생각해보자.

  • 쓰레드1 : khala == null을 확인하고 새로운 칼라 객체를 반환한다.
  • 쓰레드2 : khala == null을 확인하고 새로운 칼라 객체를 반환한다.

이 상황일경우, 칼라 객체는 2개가 되어버린다. 

 

문제 해결

그러면 이 문제를 어떻게 해결할까?

1. Synchronized

-> getInstance 앞에 Synchronized을 붙여준다.

class Khala{
	private static Khala khala
	public static synchronized Khala getInstance(){
		if (khala == null){
			khala = new Khala();
		}
		return khala;
	}
}

이렇게 되면 위의 멀티쓰레드 문제는 해결했지만, 동기화 과정에서 성능 이슈가 생길 수 있다.

성능을 신경쓰고 싶고, 객체를 만드는 비용이 비싸지 않으면 다음과 같은 과정도 시도해볼 수 있다.

2. 이른 초기화(Eager Initialization)

class Khala{
	private static Khala khala = new Khala()
	public static Khala getInstance(){
		return khala;
	}
}

이렇게 되면 동기화 성능이슈에서도 자유로워질 뿐 아니라 멀티쓰레드 환경에서도 자유롭고, 코드를 조금 더 깔끔하게 쓸 수 있다.

 

3. 이중 체크 잠금(Double Check Locking)

이중 체크 잠금은 1, 2번의 문제를 보완한 방법이다. Synchronized 블럭을 밑에서 사용함으로써 비용을 절감하고, 인스턴스 생성도 필요할 때 해줄 수 있다.

class Khala{
	private static volatile Khala khala;
	
    Khala(){}
	
    public static Khala getInstance(){
		if (khala==null){
			(synchronized(Khala.class){ // 쓰레드 두개가 한번에 들어와도 여기서 걸리게됨
				if(khala==null){
					khala = new Khala()
				}
			}
		}
		return khala;
	}
}

 

 

3번의 이중 체크에서 눈여겨 볼 곳은 바로 volatile이라는 키워드다.

더보기

Volatile을 쓰면 ?
멀티 코어 프로세서에서는 각 코어마다 Cache가 있어서 RAM의 값을 복사해온다.

그러다 보니 메모리의 값이 변경되었는데도 캐시에 저장된 값이 갱신되지 않아

각 Cache 안의 변수값이 Cache마다, 그리고 원본과도 달라질 수가 있다.

따라서 volatile을 붙여 코어의 Cache에서 RAM의 값을 복사하지 말고,

코어가 RAM의 원본을 매번 복사하게 만든다.

나중에 포스팅 하겠으나, 더욱 자세한 설명은

https://www.charlezz.com/?p=45959 을 참조하면 된다.

 

그러나 1,2,3번에도 문제가 있었으니 바로 자바 1.4 이상에서만 동작한다는 것이다.

자바의 핵심은 호환성에 있어서, 정말 크리티컬한 기술이 아니면 자바에서는 호환성이 좋은 코드를 추천한다.

그래서 자바의 추천은 다음과 같다.

 

4. 스태틱 이너 클래스(Static Inner class) 

동기화에서도 안전하고, 자바 전 구간에서 호환되는 방법이다.

public class Khala {
    private Khala(){}

    private static class KhalaHolder{
        private static final Khala khala = new Khala();
    }

    public static Khala getInstance(){
        return Khala.khala;
    }

}

클래스홀더라는 이너클래스를 만들고, 이너클래스에서 객체를 생성해 반환한다.

 

1,2,3,4의 문제점

완벽할 것 같았던 이 싱글톤 방식에도 허점이 있었으니, 바로 자바의 reflection과 serialization/deserialization이다.

Reflection은 쉽게 말하자면 class가 어떻게 구성되어있는지를 몰라도, 클래스의 객체를 생성해버릴수가 있다.

Serialization또한 바이트코드를 건드려 응용 프로그램에서 쓰는 데이터를 네트워크를 통해 전송하거나 DB 또는 파일에 저장 가능한 형식으로 바꾼다.

Deserialization은 외부 소스에서 데이터를 읽고 이를 런타임 객체로 바꾸는 반대 프로세스다.

 

그걸 또 막는 방법?

이 무슨 강퇴->강퇴반사->슈퍼방장같은 flow인가 싶지만, 다 방법이 있다.

1. enum의 사용

enum은 reflection을 못하게 만들기 때문에 reflection에 대해 안전하다.

또한 Serialize를 상속하고 있기 때문에 이또한 방어할 수 있다.

enum Khala{
	khala;
}

enum은 이렇게만 만들어 주면 된다. 그러나 enum에도 문제점이 있으니, 바로 '상속 불가능'하다는 점이다.

또한 클래스가 이미 만들어져 있어서 리소스 낭비가 있을 수 있다. 그러나 이정도 쯤이야...

 

Spring에서의 싱글톤 방식?

스프링에서 ApplicationContext등의 Stateless해야한 객체들은 늘 싱글톤 방식으로 구현해야 한다.

싱글톤 방식으로 구현할때에는 위의 reflection등을 비롯한 여러 문제가 발생하는데, 대표적으로

  • 의존관계상 클라이언트가 구체 클래스에 의존한다 ⇒ DIP 위반
  • 따라서 OCP 위반의 가능성이 높다.

등이 있다. 그러나 스프링 컨테이너가 이와 같은 문제들을 대부분 해결해준다.

@Conflguration과 @Autowired가 그 예이다.

그래서 객체 생성시 getBean으로 내용물을 꺼내오면 언제나 같은 객체가 반환된다. 이는 추후에 스프링 포스팅에서 다시 작성하도록 하겠다.

 

중복 문자를 제거하는 방법 또한 수없이 많다. 

Stream 클래스의 distinct, LinkedHashSet를 만들어 add를 하고 출력 등 갖가지 방법이 있으나

오늘은 indexOf를 이용한 풀이를 진행한다.

처음 문제를 봤을때는 String class의 replace를 이용한 풀이법을 생각했으나 쉽지 않았다.

그래서 푼 방법이 LinkedHashSet를 이용해 진행하였다.

중복 제거와 순서 저장 모두를 사용해야 하기 때문이다.

또한 이전에 포스팅했듯, String에 적재하는 방식은 스트링 풀에 계속 새로운 스트링 객체를 생성하기 때문에

메모리적으로도 좋지 않을것이라고 생각했다.

LinkedHashSet을 이용해 charAt를 저장하고, 나중에 한꺼번에 join으로 합쳤다.

조금 더 생각해보면, 훨씬 간편하면서도 나이브하게 문제를 풀 수 있다.

바로 indexOf를 사용하는 것이다.

예를 들어 apple이라는 글자가 있을때 i번째 인덱스와 charAt(i)의 인덱스는 다음과 같다

  a p p l e
i 0 1 2 3 4
indexOf(charAt(i)) 0 1 1 3 4

indexOf는 가장 처음의 인덱스만 반환하기때문에 apple의 두번째 p부터는 p의 인덱스로 1을 가져가고,

이를 이용해 중복을 제거할 수 있다.

속도 또한 밑의 방식이 조금 더 빠르게 나온다.

+ Recent posts