후기/프로젝트

패스트캠퍼스X야놀자 부트캠프: 토이 프로젝트 1 (여행 여정을 기록과 관리하는 SNS 서비스 1단계) 후기

ImKDM 2023. 9. 14. 18:17
728x90

개요

드디어 팀 프로젝트가 시작이다. 이전 두 번의 과제는 개인이었기에 다른 사람들과 함께 작업하는 것 자체가 낯설었다. 특히 한 번도 보지 못한 채 오로지 온라인으로 만난 상태로 프로젝트를 진행해 본 경험이 전혀 없었다. 따라서 어떤 상황이 벌어질지 예상이 안돼서 걱정이 됐다. 팀원들도 랜덤으로 선별되어 그룹 스터디 사람들과 겹치는 인원이 단 한 명도 없었다. 프로젝트에서 내가 어떤 도움이 될 수 있을지, 또 의견 조율은 어떻게 할지 모든 게 불확실했다. 그런 상태로 첫 프로젝트를 맞이했다.

 

토이 프로젝트의 최종 구현 목표는 "여행 여정을 기록하고 관리하는 SNS 서비스"이다. 이를 위해 총 3단계로 진행된다. 먼저 1단계는 오로지 Java 프로그램으로 구동되어야 한다. 이후 2단계는 Spring 프로젝트, 그리고 Spring boot 프로젝트로 점점 업그레이드를 시켜야 한다. 아직 Spring에 익숙하지 않은 상태라 일단 Java로만 진행되는 점에서 그나마 안심이 됐다. 그동안 공부한 Java 문법과 파일 입출력, 예외처리 기능을 잘 구현해 보고자 노력했다.

과제 요구사항

1. 여행의 여정 정보를 기록하고 조회하는 Java 애플리케이션 개발 (도메인 설계)
2. 여행 정보(출발,도착,출발시각,도착시간)과 숙박(체크인, 체크아웃) 화면 기능 설계
3. 여행 정보와 특정 여행 정보의 여정 목록은 데이터 저장 경로에 파일 형태로 저장 (CSV, json 등)
4. 여행자 관리 화면 기능 설계

 

이번에는 개인 프로젝트와 다르게 외부 API를 사용하지 않았다. 대신 중점적인 기능은 저장 결과물을 CSV와 json 파일로 저장하고, 또 불러올 수 있어야 한다. 자바에서 File을 다룰 수 있는지가 이번 미션의 주요 포인트라 생각했다. 그리고 기능 자체는 크게 어렵게 느껴지지 않았다. 사용자로부터 입력값을 받고, 해당하는 기능을 호출하면 파일을 쓰고, 읽는 기능만 구현하면 되리라 생각했다. 다만 우려스러운 부분은 "이 과정을 사람들과 함께 잘 해낼 수 있을까?"이었다.

진행사항

1.  시각화의 중요성

이런 옛말이 있듯이 시작이 가장 어렵게 느껴진다. 함께 모인 조원들은 모두 생면부지인 상태에서 프로젝트를 시작해야 했기 때문에 당연히 어색함이 감돌 수밖에 없었다. 각자 어떤 성격인지, 개발에 대한 지식이 어느 정도인지, 또 어떤 생각을 가지고 있지 몰라서 행동이 조심스러웠다. 서로 탐색전을 벌이는 도중, 한 명이 주도적으로 분위기를 끌고 갔다. 어색함을 이겨내고 먼저 나서 준 그에게 고마웠다. 그의 진행을 토대로 우리는 프로젝트에 대해 본격적으로 이야기를 나눴다.

 

프로젝트 요구사항 명세서는 수 쪽의 PDF 파일로 이뤄져 있었다. 여러 경험을 비춰봤을 때 모두 서로 다른 초점을 가지고 이야기를 나누면 흐름이 산으로 가곤 했다. 따라서 먼저 어떤 이야기를 할지 포커스를 맞추는 게 무엇보다 중요했다. 나는 팀원들에게 먼저 요구사항이 무엇인지 함께 이야기를 나눠보자고 제안했고, 모두 동의했다. 우리는 요구사항을 토대로 구현해야 할 기능들을 파악했다. 그나마 엔티티의 종류와 구조는 요구사항에 적시되어 있어 토의 주제가 기능 구현으로만 좁힐 수 있었다.

 

만약 내가 팀 프로젝트 경험이 많고 전체적인 구조를 이해하고 있었다면 적극적으로 나서서 흐름을 잡아갔을 테지만, 아쉽게도 팀 프로젝트 경험에 대해 자신감이 부족했다. 대신에 프로젝트는 아니지만 학생회 활동 및 다양한 리더 경험을 기반으로 어떤 이야기를 나눠야 하는지, 어디에 집중해야 하는지 등 각 이슈에 대한 이해도는 가지고 있었다. 고맙게도 팀원 한 명이 우리가 논의한 결과를 가지고 flow chart를 그려 함께 공유했다.

 

여행 기록 SNS Flow Chart

 

확실히 시각화가 되니 모두의 생각을 구체화하는 데 큰 도움이 됐다. 정보처리기사에서 봤던 수 많은 UML 종류가 떠올랐다. 이번 프로젝트처럼 작은 규모에서도 아주 유용했는데, 실무에서 더 복잡한 로직을 만들어갈 땐 정말 중요한 도구겠구나 깨달았다. UML이 가지고 있는 의미를 느낀 것도 좋은 학습이 됐다.

2.  MVC 패턴에 익숙해지기

각자 역할까지 분담이 완료되자 처음에 가졌던 막막함이 어느정도 해소됐다. 하지만 안심도 잠시, 실제 코드를 작성하려고 하자 또다시 막막함이 밀려왔다. 나는 혼란스러웠다. 만약 혼자서 진행했다면 내가 생각한 패키지 구조와 클래스를 생성하고 막힘없이 로직을 만들었을 것이다. 그러나 '내'가 아닌 '팀'으로 묶여 있기에 독고다이는 불가능했다. 무엇보다 MVC 패턴을 사용하라는 채점 기준은 더욱 내 마음대로 구현하는 데 족쇄를 채웠다. MVC 패턴에 대해 어느 정도 들어봤고 개념적인 설명도 가능하지만, 실제 프로젝트에서 정확히 구현하는 건 또 다른 문제였다.

 

이미 스프링으로 웹 프로젝트 경험이 있는 팀원이 있어 그의 의견을 존중하고자 했다. 이번 기회를 배움의 장으로 생각해서 MVC 패턴에 대해 익숙해지는 기회로 삼기로 마음 먹었다. 하지만 그러니 비교적 수동적인 태도가 나타났다. 아무래도 내 생각보다 경험이 많은 팀원의 의견에 힘을 실어 줬다. 물론 우리 모두 취업을 준비하는 학생 입장에서 그 누구도 완벽한 MVC 패턴을 구현하리라 생각은 하지 않았다. 따라서 무비판적으로 모든 의견을 수용할 생각도 없었다. 다만 이번 프로젝트에서 MVC 패턴에 대해 한 번이라도 더 적응하는 것이 목적이었기 때문에 열린 마음으로 받아 드렸던 것이다. 실제로 팀원들에게 질문하고 답변받는 과정에서 궁금했던 부분들이 많이 해소될 수 있었다.

 

다만 멘토가 언급했듯이, console 프로그래밍에서 MVC 패턴을 적용하는 것 자체가 최적의 선택은 아닐 수도 있겠다. 그럼에도 사용자에게 화면이 보여지는 역할과 책임을 나누는 것 자체가 최대한 객체지향적으로 고민했다는 방증이라고 생각한다.

3.  다양한 싱글톤 패턴이 있구나!

타인과 작업을 함께 하는 건 혼자하는 것보다 혼란스러울 수 있다. 왜냐하면 그동안 당연하다고 사용했던 코드의 모습과 다른 형태와 융합해야 하기 때문이다. 그 과정에서 신문물을 만나는 경험을 할 수 있는 건 큰 소득이라 생각한다. 나는 싱글톤 패턴을 사용하는 한 가지 방법을 알고 있었다. 수많은 교재에서 등장하는 방식으로 나는 그것만이 유일한 방법이라 여겨왔다. 엄밀히 따지면 이외의 방법이 존재하는지 조차 모르고 있었다. 이때 다른 팀원이 사용한 싱글톤 패턴의 형태를 목격했다. 처음에는 뭔지 몰랐는데 코드를 천천히 읽어보니 나와 다른 형태의 싱글톤 패턴이었다. 나는 즉시 구글링을 했다. 그리곤 다양한 형태의 싱글톤 패턴 적용 방법이 있다는 사실을 깨달았고 각자 존재 이유도 발견할 수 있었다. 

1.  Eager Initialization

class Singleton {
    // 싱글톤 클래스 객체를 담을 인스턴스 변수
    private static final Singleton INSTANCE = new Singleton();

    // 생성자를 private로 선언 (외부에서 new 사용 X)
    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

2Static block initialization

class Singleton {
    // 싱글톤 클래스 객체를 담을 인스턴스 변수
    private static Singleton instance;
    
    // 생성자를 private로 선언 (외부에서 new 사용 X)
    private Singleton() {}
    
    // static 블록을 이용해 예외 처리
    static {
    	try {
        	instance = new Stingleton();
        } catch (Exception e) {
        	throw new RuntimeException("싱글톤 객체 생성 오류");
        }
    }
	
    public static Singleton getInstance() {
    	return instance();
    }
}

3.  Lazy initialization

class Singleton {
    // 싱글톤 클래스 객체를 담을 인스턴스 변수
    private static Singleton instance;
    
    // 생성자를 private로 선언 (외부에서 new 사용 X)
    private Singleton() {}
    
    // 외부에서 정적 메서드를 호출하면 그제서야 초기화 진행 (lazy)
    public static Singleton getInstance() {
    	if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

4.  Thread safe initialization

class Singleton {
    private static Singleton instance;

    private Singleton() {}

    // synchronized 메서드
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

5.  Bill Pugh Solution (LazyHolder)

class Singleton {
	private Singleton() {}
    
    private static class SingleInstanceHolder {
    	private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
    	return SinleInstanceHolder.INSTANCE();
    }
}

6.  Enum

enum SingletonEnum {
    INSTANCE;

    private final Client dbClient;
	
    SingletonEnum() {
        dbClient = Database.getClient();
    }

    public static SingletonEnum getInstance() {
        return INSTANCE;
    }

    public Client getClient() {
        return dbClient;
    }
}

public class Main {
    public static void main(String[] args) {
        SingletonEnum singleton = SingletonEnum.getInstance();
        singleton.getClient();
    }
}

 

그동안 Thread safe initialization방식만 고집했었는데 이번에 Bill pugh Solution방법을 시도해볼 수 있었다. 각 방식마다 장단점이 있다는 것도 흥미로운 사실이었다.

4.  try - catch 문에 대한 오해를 풀다

프로젝트를 진행하다가 짝 코딩을 하는 기회가 왔다. 이때 내 try - catch 문을 사용한 코드를 보곤 잘못되었다고 말했다. 내가 책 보고 이해하던 흐름이 틀렸다는 것이다. 그동안 예외가 발생해서 catch 문으로 이동하면 try - catch문 아래의 코드로 내려가지 않는다고 생각해 왔다. catch문에서 해당 로직이 끝나는 거라고 이해하고 있던 것이다. 하지만 아니었다. try - catch문을 빠져나가서 그다음 문장을 계속 수행한다. 단지 catch문에서 해당 예외를 처리하지 않고 상위 메소드로 예외를 다시 던질 때(throw)에 아래 코드가 동작하지 않는 것이었다. 그리고 상위 메소드로 예외를 던질 때 특정 코드를 동작하고 싶다면 이때 finally문을 사용하면 된다.

 

짝 프로그래밍을 통해 소득이 많았던 좋은 경험을 했다.

5.  예외처리를 재귀 호출로 풀다

사용자로부터 입력값을 받을 때 유효한 값인지 판단하는 로직을 짜는 건 항상 고민이었다. while문을 사용해 조건에 맞지 않으면 계속 반복시키는 방식은 고전적이지만 뭔가 마음에 들지 않았다. 코드의 가독성뿐만 아니라 로직을 따라가는 데도 불편했다. 더 객체지향적인 방식이 있으리라 생각했지만 생각보다 떠오르지 않았다. 아.. 어디서 코드를 봤는데...

 

답은 생각보다 가까이 있었다. 지난 개인 프로젝트에서 다른 사람이 짠 코드에서 힌트를 얻을 수 있었다. View 클래스 내에 여행 시작을 받는 메소드에서 사용자로부터 입력값을 받는다. 그리고 바로 if문에서 내가 원하는 값인지 아닌지 확인한다. 만약 맞지 않는 값이라면 다시 getStartDate() 메소드를 호출한다. 재귀적으로 반복 호출하는 것이다. 아주 깔끔하게 해결될 수 있었다. 개인적으로 만족!

 

private String getStartDate() {
    System.out.print("여행 시작일을 입력해주세요: ");
    String startDate = scan.nextLine();
    if (isBlank(startDate)) {
        System.out.println(NO_BLANK);
        return getStartDate();
    }
    return startDate;
}

5.  결과

약 5일간 힘을 모아 만든 프로젝트가 결실을 맺었다.

 

여행 기록 기능 구현
여행 조회 기능 구현
여정 조회 기능 구현

 

많은 부분이 낯설었지만 온라인으로 하나의 결과물을 만들었다는 점에서 뿌듯하다. 과정에서도 내가 몰랐던 점들을 많이 배울 수 있었다. 이번 경험을 통해 배움의 과정은 두려움이 아니라 설레임이라는 사실을 다시금 깨달았다. 다음 프로젝트도 기대해 봐야겠다.