MINIWIKI
CareerSideProjectBook&Study
  • ⚡README
  • 😃ME
    • Review
      • 2025 OKR & 회고 - 회사 없이도 먹고살 수 있는 상태가 된다
        • 2025년 19주차
        • 2025년 18주차
        • 2025년 17주차
        • 소설쓰기의 쓸모
        • 2025년 15주차
        • 2025년 14주차
        • 요즘 회사생활
        • 첫 페이지 작성!
        • 큰 코 다쳤다
        • 오랜만에 좋았던 하루
        • 악순환과 반복실패
        • 2025년 12주차
        • 2025년 11주차
        • 2025년 3월 6일
        • 2025년 3월 4일
        • 2025년 3월 1일
        • 2025년 2월 회고
        • 2025년 1-2월 책, 영화, 음악
        • 2025년 1-2월 회고 (PM)
        • 2025년 1-2월 회고 (콘제품)
          • (Merged) 2025 비즈니스
        • 2025년 1-2월 회고 (삶/사람)
        • 2025년 1-2월 회고 (기본)
        • (25.02) 고객 피드백 받기
        • 다시 전략 수정
        • 머리 속 복잡한 것들 끄적끄적
        • 변하지 않는 핵심 철학
        • 개별화 능력을 이용하는 방법
        • 파고들기
        • 예술가와 사업가
        • 강점
        • PM으로서의 전문성
        • 부동시
        • 이게 다 무슨 소용인가
        • 내가 가장 잘 전할 수 있는 메시지
        • 연말인사 타이밍
        • Attitude는 옷부터
        • 다시 시작
      • 2024 회고
        • 2024년 12월 4주차 (52/52)
        • 원한다고 생각했던 것들
        • 2024년 12월 3주차 (51/52)
        • 회사 vs. 퇴사
        • 2024년 12월 2주차 (50/52)
        • 2024년 11월 4주차 (47/52)
        • 2024년 11월 3주차 (46/52)
        • 2024년 11월 1주차 (44/52)
        • 혓바늘
        • 2024년 10월 3주차 (42/52)
        • 그냥, 요즘하고 있는 생각들
        • 2024년 10월 1주차 (40/52)
        • 2024년 9월 4주차 (39/52)
        • 2024년 9월 3주차 (38/52)
        • 2024년 9월 2주차 (37/52)
        • 2024년 9월 1주차 (36/52)
        • 2024년 8월 4주차 (35/52)
        • 잃어버린 보물창고
        • 분기별 프로젝트
        • 강점검사
        • 글쓰기
        • 이상적인 하루
        • 나와 아프리카
        • 한 때 나에게 힘이되었던 문장들
      • 2023 회고
        • 2023년 12월 5주차
        • 2023년 12월 4주차
        • 2023년 12월 3주차
        • 2023년 12월 2주차
        • 2023년 12월 3일
        • 2023년 12월 1주차
        • 2023년 11월 29일
        • 2023년 11월 28일
        • 2023년 11월 27일
        • 2023년 11월 18일
        • 2023년 11월 15일
        • 2023년 11월 12일
        • 2023년 11월 11일
        • 2023년 11월 1주차
        • 2023년 10월 3주차
        • 2023년 9월 4주차
        • 2023년 9월 3주차
        • 2023년 9월 2주차
        • 2023년 9월 1주차
        • 2023년 8월 4주차
        • 2023년 8월 2주차
        • 2023년 8월 1주차
        • 2023년 7월 4주차
        • 2023년 7월 3주차
        • 2023년 7월 2주차
        • 2023년 상반기 회고
        • 나태하고 욕심많은 인간은 어떻게 살아야 하나
        • 책 <어떻게 살아야 하는가>
        • 책 <당신은 결국 무엇이든 해내는 사람>
        • 복잡계를 살아가는 단순한 사람
        • 책 <모든것이 되는법>
        • 글로 신뢰를 얻었던 경험들
        • 기획은 나를 찾아가는 과정
        • 나는 왜 살아가는가
        • 장항준 감독으로부터 배우는 "삶을 대하는 자세"
        • 개발자가 말하는 감정에도 분석이 필요한 이유
      • 2022년 회고
        • problem map 작성하기
        • 삶에서 내가 해결하고 싶은 문제 (2)
        • 삶에서 내가 해결하고 싶은 문제 (1)
        • <삶의 문제> 지도 다시 꺼내보기
        • 지도 위의 29살
        • 매번 시간계획을 망치는 MBTI 'P형 인간'을 위한 5단계 인생관리법
        • 당신은 왜 프로그래밍을 공부하는가?
        • 아무 것도 아닌 내가 글을 쓰는 이유
        • 책 <여행의 이유>
        • 책 <붕대감기>
      • 2021년 회고
    • Career
      • [미리캔버스] AI 제품 PM
        • 선택과 집중
        • 어쩌면 내가 틀릴 수도 있다는 생각
        • 뾰족한 사람들과의 협업
        • AI기능 PPT로 온보딩
        • AI 제품에서 가장 중요한 것
      • [미리캔버스] 앱 PM
      • [미리캔버스] 소상공인 제품 PM
      • [미리캔버스] 2.0 PM
      • [홀로스탠딩] 백엔드 개발
      • [청년5.5] 안드로이드 개발
      • [가축대출사업] NGO Project PM
    • Insight
    • Interview
    • Public Writing
  • SIDE PROJECT
    • [Youtube] 메이킹필름
      • [Product] 청춘집 프로젝트
        • (v24.11) 청춘집 JTBD
          • (구) 청춘집 실행계획
          • (구) 플레이리스트 기획
            • 데이식스 전곡 타임라인
            • 챕터 구성
        • (v25.01) 청춘집 JTBD
          • 아이돌 굿즈 시장 조사 (공식)
          • 아이돌 굿즈 시장 조사 (비공식)
        • 제작 준비
          • 레퍼런스 - 오프라인 시집
          • a5 책 만들기
    • [Youtube] 마포구타자기
      • [mptw] JTBD
        • IKIGAI
      • [mptw] 채널 설정
        • 채널 이름 후보군
      • 시리즈 [읽는음악]
        • [읽는음악] 백로그
          • 노래 가사 콘텐츠 레퍼런스
        • ep1. 파노라마 - 이찬혁
          • 이찬혁 <ERROR>
        • ep2. 마지막 인사 (feat. 청하) - 이찬혁
        • ep3. 나의 바다에게 - 도영
        • ep4. Dattom - 백예린
        • ep5. REBEL HEART - IVE
        • ep6. Either way - IVE
        • ep7. 너와의 모든 지금 - 재쓰비(JAESSBEE)
        • ep8. 예뻤어 - DAY6
        • 6주차. 데이식스 시리즈
    • [IT] 공적인사적모임 플랫폼
      • 1. 우리 조직의 얼굴을 만들자
      • 2. 내 생에 첫 기획서 만들기 (feat. QA Driven Development)
    • [Meet] 공적인사적모임
    • [Youtube] 이상한나라의 개발자할무니
    • [Study] Disquiet PM 스터디 쿨피스
    • [IT] 서울 빵 맛집 잘알 테스트
    • [Meet] 얼리버드 모닝클럽
      • 홍보를 곁들인 2주일 운영후기
  • 잡학사전
    • 와인 원데이 클래스
    • 소설쓰기
      • <책> 소설쓰기의 모든 것 1 - 플롯과 구조
      • 유튜브 - 소설 쓰는 법
      • 강의들
      • 작가가 되려면 어떻게 해야해
    • AI
      • 생성형 AI
    • ComfyUI
      • Stable Diffusion
      • ComfyUI 준비, 설치, 설정
      • Module 구조에 대해 이해하기
      • ComfyUI
      • Core Node
    • 작사
      • 작사가 되는 법
    • 유튜브
      • 유튜버 스토리님의 부캐 성장기
      • 주언규 유튜브 초보편 (클래스101)
      • 주언규 유튜브 왕초보 편
    • 경제
      • 연금저축펀드
    • ChatGPT
    • 크롤링
      • Automatio
      • Octoparse
    • 노코드
      • 북마크 & 노코드 서비스 목록
  • PRODUCT&BUSINESS
    • Service Planning/Analysis
      • 브런치시리즈 <개발보단 고객개발>
      • baemin mart
        • 1. 시작
        • 2. 우아한형제들 & 배민상회
        • 3-1. [인터뷰] 포항에서 치킨집을 운영하시는 최사장님
        • 3-2. [인터뷰] 부산에서 족발 프렌차이즈를 운영하시는 이사장님
        • 4. <아프니까 사장이다> 커뮤니티 데이터 분석
        • 5. 문제정의 & 개선 가설
        • 6. 결론 - 역기획서
      • careerly
      • meetme
      • 배달의 민족 역기획 사례
      • 당근마켓 역기획 사례
      • 도그냥 님이 말하는 진짜 역기획
      • 도그냥의 역기획 스터디법
      • 책 <현업 기획자 도그냥의 서비스 기획 스쿨>
      • 기획서 작성하기
    • Business/Growth
      • Unsexy Business 뉴스레터에서 얻는 인사이트
      • 책 <원씽>
      • 책 <아프리카 스타트업>
      • 책 <유난한 도전>
      • 책 <함께자라기>
      • 책 <나는 돈 없어도 사업을 한다>
      • 책 <나는 장사의 신 은현장이다>
      • 책 <왜 사업하는가>
      • 책 <왜 일하는가>
      • 이제는 피칭도 유튜브로
      • 세컨드 브레인이 필요한 이유
      • 책 <타이탄의 도구들>
      • 책 <역행자>
        • <역행자> 역행자의 7단계 모델 복습
        • <역행자> 운명을 거스르는 역행자의 7단계 모델
      • 책 <월급쟁이로 시작한 38살 그녀는 어떻게 30억을 벌어 파이어족이 되었을까?>
      • 책 <파리에서 도시락을 파는 여자>
      • 책 <존리의 금융문맹탈출>
      • 책 <돈의 감각을 길러주는 경제 지식 첫걸음>
        • 금리
        • 환율
        • 주식
        • 채권
        • 부동산
        • 연금
        • 경제정책
        • 규제
        • 경제위기
    • Product-Market Fit
      • 브런치 북 <개발보단 고객 개발>
      • 책 <아이디어 불패의 법칙>
      • 고이장례연구소
      • 글쓰기로 PMF 검증하기
      • 연대 송도 캠퍼스의 40%가 사용한 서비스
      • 어웨이크코퍼레이션의 김민준 님
      • 드로우 마이 브랜드
      • 노코드로 PMF 찾는 방법
    • UI/UX
      • UX Writing Workshop
        • 4. 고객과의 관계형성 - 차별점 강화
        • 3. 비즈니스 임팩트를 만드는 글쓰기
        • 2. 후킹한 문장으로 고객 행동 이끌기
        • 1. 쉽고 정확한 문장으로 문제해결
        • What is UX Writing?
        • Reference
      • UX/UI 관련 유용한 사이트 모음
    • PM/PO
      • 책 <프로덕트 매니지먼트>
      • 책 <인스파이어드>
      • PM Wiki
      • 당신과 팀을 성장시킬 PM 직무가이드
      • PO 미신, 파랑새를 찾아서 - CPO 김용훈
      • 개발자가 생각하는 좋은 PM 나쁜 PM
      • 프로덕트 매니저는 뭐하는 사람인가
      • 토스 리더가 말하는 PO가 꼭 알아야할 개념 (2)
      • 토스 리더가 말하는 PO가 꼭 알아야할 개념 (1)
      • 책 <조직을 성공으로 이끄는 프로덕트 오너>
        • <프로덕트 오너> PO의 시간관리법
        • <프로덕트 오너> PO가 데이터 기반으로 일할 수 밖에 없는 이유
  • DATA
    • Database
      • 이 위키를 만드는데 참고한 자료들
      • 데이터 기반 의사결정
      • 데이터베이스의 종류
      • 트랜잭션과 무결성
      • 트랜잭션, 커밋, 롤백, 트랜잭션 전파
      • ERD, entity relationship diagram
      • 기본 3 - 관계, 키
      • 기본 2 - 필드, 레코드, 타입
      • 기본 1 - 엔티티, 릴레이션, 속성, 도메인
    • SQL
      • Sub Query
      • JOIN
      • 데이터 정렬셋과 유니코드
      • 자료형
      • DDL, DML
      • SELECT
      • SQL
    • MySQL
      • MSQL to MySQL Data Migration
      • MySQL Server 다운로드, 로그인
      • helpful commands
      • 문자열 자르기 SUBSTR(column, startIdx, length)
      • 특정 값을 ORDER BY 특정 값 우선 정렬 하기 (ORDER BY FIELD)
      • 이것이 MySQL이다
    • H2
      • ‼️h2 in-memory-db Table not found (this database is empty) 해결방법
  • Dev-General
    • Webmark
    • Open Source
      • 나의 첫 opensource contribution 경험기
    • Dev-Insight
      • Event
        • YOUTHCON 2022
        • INFCON 2022
      • 책 <누워서 읽는 알고리즘>
      • 책 <나는 LINE 개발자입니다>
      • 서비스에 대해 개발자가 가져야할 생각들
      • AI 시대에서 결국 살아남는 것
      • AI 시대에 개발자가 살아남는 방법
      • 주니어를 넘어서, 성장하는 개발자의 길 (인프런)
      • 아마추어와 프로의 차이
      • 개발자의 개발공부에 대하여
      • 서비스에 대해 개발자가 가져야할 생각들
      • 좋은 개발자와 인맥을 만든 노하우
      • 개발자 취업기/이직기 모음
        • 라인게임즈 백엔드 개발자 경선님
        • OKKY 미니세미나 <비전공 학원출신 SI개발자, 유명스타트업 들어간.ssul> 참석 후기
        • 비전공자에서 2억받는 아마존 엔지니어가 되기까지
        • IT 대기업 100% 합격하는 방법
  • 🏗️computer science
    • Algorithm & Data Structure
      • About this page
      • Test Review
        • Page 1
      • Big-O
        • 빅오표기법의 문제풀이
        • 피보나치 수열의 시간복잡도
      • Bit Operation
        • bit masking
      • Math
        • 합공식 / 누적합
        • 피보나치 수
        • 약수찾기
        • 소수찾기
          • 백준 1978 소수찾기
          • 백준4948 베르트랑 공준
          • 백준 8393 합
          • 백준 1929 소수구하기
        • 최대공약수 / 최소공배수
          • 백준 2824 최대공약수, BigInteger
          • 백준 2609 최대공약수, 최소공배수
        • 순열과 조합
          • 백준 15649 N과 M
        • 그 외 개념 정리
      • Recursion
        • N Queens problem
        • counting cells in a blob
        • recursion 응용 - 미로찾기
        • 순환 알고리즘의 설계
        • 순환적으로 사고하기
        • 백준 17478 재귀함수가 뭔가요
        • 백준 10870 피보나치수 5
      • Sort
        • java 에서의 정렬
        • radix sort
        • sorting in linear time
        • comparison sort 에서 최상의 시간복잡도
        • priority queue
        • heap sort
        • quick sort
        • merge sort
      • Array and List
        • 표준 라이브러리
      • Linked list
      • String
      • Stack
        • 백준 1874 스택수열
        • 백준 10828 스택 구현하기
      • Queue
        • 백준 10845 큐 구현하기
      • Heap
        • 백준 11298 절대값힙
        • 백준11279 최대힙
        • 백준1927 최소힙
      • Deque
      • Tree and Binary tree
        • Tries
        • Red-Black Tree
        • Binary Search Tree
      • Search
        • 완전 탐색
        • 이분탐색
      • Graph
        • 최단경로
        • MST 2 - prim 의 알고리즘
        • MST 1 - Kruskal 의 알고리즘
        • MST, minumum spanning tree
        • DAG, Directed Acyclic Graph
        • DFS, Depth First Search
        • BFS, Breadth First Search
      • Dynamic Programming
        • Knapsack problem
        • LCS, Longest Common Subsequence
        • matrix chain
        • 행렬 경로 문제
        • 백준 1003 피보나치 함수
        • 백준 9461 파도반 수열
        • 백준9251 LCS
      • Greedy
      • Implementation
      • LIS, Longest Increasing Subsequence
      • Two Pointer
      • Line Swipping
      • Fenwick tree
      • Backtracking
    • Computer Structure
      • 이 위키를 만드는데 참고한 자료들
      • 그래서 컴퓨터는 어떻게 동작하나요?
      • 컴퓨터의 구성
      • 컴퓨터의 역사
      • 컴퓨터 구성요소의 기능 및 이해
      • 중앙처리장치 - 마이크로 명령 - 입출력과 인터럽트
      • 중앙처리장치 - 기본 컴퓨터 프로그래밍
      • 중앙처리장치 - 프로그래밍 언어와 실행
      • 파이프라인과 벡터처리 - 데이터의 종속성 - 병렬처리와 파이프라인
      • 파이프라인과 벡터처리 - 파이프라인 구조 - 데이터/구조
      • 파이프라인과 백터처리 - 산술&명령어 파이프라인
      • 파이프라인과 벡터처리 - 파이프라인 CPU의 성능분석
      • 메모리 구조 - 메모리 시스템의 이해
      • 메모리 구조 - 효율적인 메모리 관리 정책
      • 메모리 구조 - 컴퓨터 성능 개선을 위한 메모리 관리
      • 입출력구조 - 시스템 BUS 구성 및 제어
      • 입출력 구조 - 입출력(I/O) 연결과 주소 지정
      • 입출력 구조 - 입출력 수행과 인터럽트
      • 병렬컴퓨터 구조와 성능분석 - 멀티 프로세서
      • 병렬 컴퓨터 구조와 성능 분석 - 시스템 성능 분석과 개선
    • This Is Coding Test 2021
      • 1. 출제 경향 & 파이썬 문법 부수기
      • 2. 그리디 알고리즘 & 구현
      • 3. BFS & DFS
      • 4. 정렬 알고리즘
      • 5. 이진탐색
      • 6. 다이나믹 프로그래밍
      • 7. 최단경로 알고리즘
      • 8. 기타 그래프 이론
      • 9. 코딩테스트에서 자주 출제되는 기타 알고리즘
      • 10. 개발형 코딩테스트
    • Operating System
      • 이 위키를 만드는데 참고한 자료들
      • 운영체제란, Introduction to Operating Systems
      • 컴퓨터 시스템의 구조, Structure of Computer System
      • 프로그램의 실행, Program Execution
      • 프로세스, Process
      • 쓰레드, Thread
      • 프로세스의 생성과 종료, Start and End of Process
      • 프로세스 시스템 콜과 프로세스간의 협력, System call and Interprocess Communication
      • CPU Scheduling
      • CPU Scheduling Algorithm
      • Process Synchronization Problem
      • Initial Attempts to Solve Process Synchronization Problem
      • semaphore 와 monitor 로 synchronization 해결하기
      • 데드락, Deadlock
      • 메모리 관리, Memory Management
      • Memory Allocation
      • Virtual Memory
      • Virtual Memory 2
      • File System
      • File Systems Implementation
      • Disk Management & Scheduling
    • Network
      • 이 위키를 만드는데 참고한 자료들
      • 대규모 트래픽으로 인한 서버 과부하 해결방법
      • 유선 LAN과 무선 LAN
      • 네트워크를 이루는 장치 (L1, L2 .. L7)
      • REST API
      • HTTP 매서드
      • HTTP 상태코드
      • 직렬화와 역직렬화
      • 로그인 구현방식 2. 토큰 기반 인증방식
      • 로그인 구현방식 1. 세션 기반 인증방식
      • 웹 브라우저의 캐시 - 공통점과 차이점
      • 웹 브라우저의 캐시 - 쿠키
      • HTTP header
      • 웹 브라우저의 캐시 - 세션 스토리지
      • 웹 브라우저의 캐시 - 로컬스토리지
      • browser rendering
      • HTTPS 와 TLS - TLS 핸드쉐이크
      • HTTPS 와 TLS - 암호화
      • HTTP History
      • www.naver.com 을 주소창에 입력하고 화면에 나타나기까지의 과정
      • IP 주소 - 공인 IP와 사설 IP
      • IP 주소 - Classless,Subnet Mask, Subnetting
      • IP 주소 - Classful IP Addressing
      • IP 주소 - IPv4, IPv6
      • IP 주소 - 이진수 이해하기
      • IP 주소, MAC 주소, ARP, RARP
      • 라우팅
      • TCP 4way handshake and TIME_WAIT
      • TCP 3way handshake
      • TCP/IP - internet layer
      • TCP/IP - Transport Layer
      • TCP/IP - Application Layer
      • TCP/IP - MTU, MSS, PMTUD
      • TCP/IP 4계층, OSI 7 layer
      • 네트워크의 분류 - LAN, MAN, WAN
      • 네트워크 토폴로지와 병목현상
      • 네트워크의 기초 3
      • 네트워크의 기초 2
      • 네트워크의 기초
    • Linux
      • reference
      • sudo apt-get install / uninstall
      • vim
      • linux basic command
    • Design Pattern
      • 이 위키를 만드는데 참고한 자료들
      • static 을 자주 사용하게 되었을 때의 단점
      • 자바스크립트의 class와 static
      • 프로그래밍 컨텍스트
      • 의존성 주입 vs. 전략패턴
      • flux pattern
      • Spring MVC 패턴 적용 사례
      • MVC, MVP, MVVM pattern
      • 프록시 패턴
      • 옵저버 패턴
      • 전략패턴
      • 의존성 주입과 의존 관계 역전 원칙
      • 이터레이션 패턴
      • 추상 팩토리 매소드 패턴
      • 팩토리 메소드 패턴
      • 싱글톤 패턴
      • 디자인 패턴, 라이브러리와 프레임워크의 차이
    • Programming Basic (Go)
      • 이 위키를 만드는데 참고한 자료들
      • 트랜지스터, Trangister
      • 논리소자, Logic Element
      • 튜링과 폰 노이만, Turing and Von Neumann
      • 컴퓨터의 원리, Computer Principle
      • 프로그래밍 언어, Programming Language
      • 컴파일러와 동적언어, Compiler and dynamic language
      • golang
      • hello, world
      • variable
      • variable 2
    • Base Knowledge
      • 이 위키를 만드는데 참고한 자료들
      • 신기술 도입시 고민해야할 점(feat. react.js vs. vue.js)
      • 정적 타입 시스템의 필요성
      • 도커, 컨테이너
      • 클라우드, Saas, IaaS, PaaS
      • SSO
      • RBAC
      • OAuth2.0
      • REST API 사용을 위한 인증 방법 4가지
      • API
      • Data Format - XML
      • Data Format - JSON
  • ☕Java/Spring
    • Java
      • Java Code Convention
      • Java 버전별 특징 (v1-v19)
      • java.lang.Math
      • List 4가지의 초기화 방법
      • HashMap 4가지의 정렬 방법
      • 어노테이션 프로세서 정리하기
      • Annotation Processor 로 없는 소스코드 생성하기
      • lombok 은 어떻게 동작하는 것일까?
      • 다이내믹 프록시 정리하기
      • 클래스의 프록시
      • 다이내믹 프록시
      • 프록시 패턴은 무엇인가
      • Spring Data JPA 는 어떻게 동작할까?
      • reflection api 정리
      • reflection api 이용하여 spring ioc container 만들기
      • reflection api
      • spring dependency injection 은 어떻게 동작할까
      • 바이트 코드 조작하기
      • java bytecode 를 조작해 테스트 코드 커버리지 확인하기 (feat.jacoco)
      • Class Loader
      • JVM 의 구조
      • java, jvm, jdk and jre
      • synchronized
      • java string.split(".") 오류
    • Java 8
      • 이 위키를 만드는데 참고한 자료들
      • Metaspace
      • Parallel 정렬
      • Annotation
      • CompletableFuture
      • Date and Time
      • Optional
      • Stream
      • interface의 default 메소드와 static 메소드
      • 인터페이스의 변화
      • 함수형 인터페이스
      • java 8 소개
    • Spring Framework
      • Spring 3.0 준비하기
      • 특정 매소드만 transaction 처리하기
      • 스프링 프로젝트 시작하기
      • 스프링이란 무엇인가
      • 스프링 핵심 기술의 응용
      • AOP 2
      • AOP 1
      • 서비스 추상화 2
      • 서비스 추상화 1
      • 예외
      • 템플릿
      • 테스트
      • 오브젝트와 의존관계
      • 스프링이란
    • Spring Boot
      • [Gradle]UncheckedIOException
      • java19 + spring 3.0.5 + gradle 7.4.1 에서 프로젝트 gradle 설정하기
      • [리뷰] Gradle 멀티 프로젝트 관리
      • [리뷰] 멀티모듈 설계 이야기 with Spring, Gradle
    • JPA/QueryDSL
      • querydsl 을 쓰는 이유
      • JPA querydsl에서 json array 로 된 컬럼에 조건 적용하기
      • querydsl 에서 mysql order by field() 사용하기
  • 🏰Infrastructure
    • InfraWorkshop
      • 이 위키를 만드는데 참고한 자료들
      • aws로 안정적인 인프라 만들기 2
      • aws로 안정적인 인프라 만들기 1
      • 어플리케이션 진단하기
      • 서버 진단하기
      • 부하 테스트
      • 웹 성능 개선하기
      • 웹 성능 진단하기
      • <aws로 그럴듯한 인프라 만들기> 회고와 피드백
      • aws로 그럴듯한 인프라 만들기 3 - 배포스크립트
      • aws로 그럴듯한 인프라 만들기 2 - 배포하기
      • aws로 그럴듯한 인프라 만들기 1 - 네트워크 망 구성
      • docker container
      • connection check
      • network segmentation
      • cloud 서비스를 사용한다는 것
    • AWS
      • AWS IAM
      • AWS CodePipeline 으로 배포 자동화하기 (1)
      • AWS CodePipeline 으로 배포 자동화하기 (2)
  • 🪄Test
    • TDD
      • 이 위키를 만드는데 참고한 자료들
      • [2주차] 로또 과제 강의를 듣고나서
      • [1주차] 자동차 경주 과제 강의를 듣고나서
      • TDD, 리팩토링이란?
      • 가장 쉽게 TDD 시작하는 방법
      • 의식적인 연습과 학습 테스트
      • TDD 에 집착해야하는 이유
      • 공부하는 자세
    • AssertJ
      • 이 위키를 만드는데 참고한 자료들
    • JUnit
      • 이 위키를 만드는데 참고한 자료들
      • Junit 기본 개념
  • 😎OTHERS
    • Helpful Command
      • Mac 에서 특정 포트 검색, 종료
      • crontab
    • Llibrary
    • IntelliJ
      • 내가 좋아하는 커스텀 세팅
    • GIT
      • Github ID/Token 한번 입력 후 저장하기
      • Github Actions
      • github organization private repository push 안될 때 (not found issue)
      • commands
      • git commit convention
    • Logging
      • logback + webfilter 로 로그설정
      • ‼️log4j 보안 이슈
    • Postman
      • postman 의 header에서 언더바(_) 변수 인식 안되는 현상
Powered by GitBook
On this page
  • 템플릿
  • 예외처리 코드 추가하기
  • 문제점
  • 분리와 재사용을 위한 디자인 패턴 적용
  • 1. 매소드 추출 방법 ❌
  • 2. 템플릿 메소드 패턴 적용 ❌
  • 3. 전략 패턴 적용 🙆
  • 4. DI 적용을 위한 클라이언트/컨텍스트 분리 🙆
  • JDBC 전략 패턴의 최적화
  • 로컬 클래스
  • 익명 내부 클래스
  • 컨텍스트와 DI
  • 클래스 분리
  • 빈 의존관계 변경
  • JdbcContext 의 특별한 DI
  • 템플릿과 콜백
  • 템플릿/콜백의 특징
  • 콜백의 분리와 재활용
  • 콜백과 템플릿의 결합
  • 템플릿/콜백의 응용 - Calculator
  • 제네릭스를 이용한 콜백 인터페이스
  • 스프링의 JdbcTemplate
  • 정리

Was this helpful?

  1. Java/Spring
  2. Spring Framework

템플릿

Previous예외Next테스트

Last updated 2 years ago

Was this helpful?

자바, 스프링 개발자들의 종착역이자, 주기적으로 다시 돌아오는 그곳, <>을 드디어 읽는다.

한창 개발 걸음마를 막 떼고 스프링으로 아장아장 기어다니며 CRUD를 할 때즈음, 토비의 스프링을 찬양하고 있는 많은 개발자들을 보았고, 역시 지식이 깊어지려면, 책을 통해서 지식을 정리하고 깊이를 다지는 시간이 필요하겠구나, 생각했다. 하지만, 개발을 이제 막 배운 그때의 나에게 토비 책은 너무나도 무시무시했고, 토비책을 읽다가 개발을 포기한 사람들의 증언도 여럿 읽고 나니, 이 책은 개발자로서 레베루업이 필요할 때즈음, 다시 꺼내봐야겠다고 생각했다.

그리고 지금이다. 퇴사 후, 이직을 준비하는 동안, 지식의 깊이를 좀 더 다져놓고 싶다는 생각이 들었고, 집앞 도서관에서 (아무도 빌려가지 않는) 토비님의 책을 빌려서 읽기 시작했다. 처음에는 가볍게 읽기 시작했지만, 읽을 수록 인사이트가 쌓여, 기록하면서 제대로 읽고 싶어졌다. 그래서 나의 세컨 브레인인 이곳 블로그에 짧은 글들로 그 내용을 적어보며, 토비님으로부터 얻은 인사이트를 내것으로 만들어보고자 한다.

템플릿

  • 개방폐쇄원칙을 보면, 코드 내에서도 변경을 통해 기능이 다양해지려는 성질도 있고, 고정되어 변하지 않으려는 성질도 있다는 것을 알 수 있다. 변화의 특성이 서로 다른 부분들을 구분하고, 각가 다른 목적과 다른 이유에 의해 다른 시점에 독립적으로 변경될 수 있는 효율적인 구조를 만들어주는 것이 바로 개방폐쇄 원칙인 셈!

  • 템플릿의 경우, 이렇게 바뀌는 성질이 다른 코드 중에서 변경이 거의 일어나지 않으며 일정한 패턴으로 유지되는 특성을 가진 부분을 자유롭게 변경되는 성질을 가진 부분으로부터 독립시켜 효과적으로 활용할 수 있도록 하려는 방법이다.

예외처리 코드 추가하기

  • 기존까지 진행된 리펙토링을 위해 많은 구조 개선이 있었지만, 한 가지 문제점, 예외처리가 되지 않았다는 문제점이 있었다. 특히나 DB 커넥션과 같은 제한적인 리소스를 공유해서 사용하는 서버에서 동작하는 JDBC 코드에서 예외처리는 필수적이다. 중간에 예외가 발생해서 리소스가 제대로 반환이 안될 경우, 계속 문제가 누적되어 나중에는 서버가 멈추는 현상까지 갈 수 있기 때문이다.

  • 아래와 같이 예외처리를 추가해보았다.

public class UserDao {
    private DataSource dataSource;

    public UserDao() {
    }

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    
    ...

    public void deleteAll() throws SQLException {
        Connection c = null;
        PreparedStatement ps = null;

        try {
            c = this.dataSource.getConnection();
            ps = c.prepareStatement("delete from users");
            ps.executeUpdate();
        } catch (SQLException var15) {
            throw var15;
        } finally {
            if (ps != null) {
                try {
                    ps.close();
                } catch (SQLException var14) {
                }
            }

            if (c != null) {
                try {
                    c.close();
                } catch (SQLException var13) {
                }
            }

        }
    }
    
    ...
}

문제점

  • 위의 코드 상 문제점은 c.prepareStatement("delete from users"); 사실상 이 구절만을 제외하고는 UserDao 내의 다른 매소드(add(), get(), getCount() 등) 에서 위의 구절이 반복된다는 것이다.

  • 복사 붙여넣기를 하는 것은 문제를 해결할 수 없다. 도중에 한 줄의 코드라도 실수로 잘못 붙여넣거나 빠뜨리게 되면, connection 자원은 계속 누수가 생길 것이고, 언젠가는 서버가 멈추게 된다.

  • 이 상황을 효과적으로 해결하기 위해서는 변하는 부분과 변하지 않는 부분을 명확하게 파악하고 일종의 템플릿화를 함으로써 접근할 수 있다.

분리와 재사용을 위한 디자인 패턴 적용

생각해보면, 변하는 부분과 변하지 않는 부분은 명확하다.

  • 변하는 부분 : ps = c.prepareStatement("delete from users");

  • 변하지 않는 부분 : 나머지

1. 매소드 추출 방법 ❌

  • 그렇다면 우선 변하는 부분을 메소드로 빼는 방법을 선택해볼 수 있다.

  • 하지만 이렇게 진행할 경우, 매소드의 내용이 다른 곳에 재사용할 수 있는 것이어야 하는데, 이 경우는 반대로 매소드 이외의 부분을 재사용하고, 매소드 부분은 DAO 로직마다 새롭게 만들어야 한다.

public class UserDao {
    private DataSource dataSource;

    public UserDao() {
    }

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    
    ...

    public void deleteAll() throws SQLException {
        Connection c = null;
        PreparedStatement ps = null;

        try {
            c = this.dataSource.getConnection();
            ps = makeStatement(c);        //매소드로 추출한 부분! 
            ps.executeUpdate();
        } catch (SQLException var15) {
            throw var15;
        } finally {
            if (ps != null) {
                try {
                    ps.close();
                } catch (SQLException var14) {
                }
            }

            if (c != null) {
                try {
                    c.close();
                } catch (SQLException var13) {
                }
            }

        }
    }
    
    ...
    
    private PreparedStatement makeStatement(Connection c) throws SQLException {
        PreparedStatement = ps;
        ps = c.prepareStatement("delete from users");
        return ps;
    }
}

2. 템플릿 메소드 패턴 적용 ❌

  • 템플릿 매소드 패턴은 상속을 통해 기능을 확장해서 사용하는 것

  • 변하지 않는 부분은 슈퍼클래스로 두고, 변하는 부분을 추상 매소드로 정의해두어 서브클래스에서 오버라이드하여 새롭게 정의해 쓰도록 한다.

public class UserDao {
    ...
    
    abstract private PreparedStatement makeStatement(Connection c) throws SQLException;
}


public class UserDaoDeleteAll extends UserDao {
    
    protected PreparedStatement makeStatement(Connection c) throws SQLException {
        PreparedStatement ps = c.prepareStatement("delete from users");
        return ps;
    }
}
  • 하지만 이렇게 될 경우, 새로운 DAO 로직이 늘어날 때마다 UserDao 클래스를 오버라이딩하는 새로운 클래스를 만들어야 한다는 단점이 있다.

    • UserDao

      • UserDaoAdd

      • UserDaoDeleteAll

      • UserDaoGet

      • UserDaoGetCount

      • ...

  • 또한 확장구조가 이미 클래스를 설계하는 시점에 고정되어버린다. 이렇게 되면 관계의 유연성이 떨어지게 된다.

3. 전략 패턴 적용 🙆

  • 개방폐쇄원칙을 잘 지키면서 템플릿 매소드 패턴보다 유연하고 확장성이 뛰어난 것이 바로 전략패턴이다.

  • 전략 패턴에서는 오브젝트를 아예 둘로 분리하고, 클래스 레벨에서는 인터페이스를 통해서만 의존하도록 만드는 방법이다.

  • 확장에 해당하는 변하는 부분을 별도의 클래스로 만들어 추상화된 인터페이스를 통해 위임하는 방식.

예) deleteAll()

예를 들어 deleteAll() 을 통해서 보자면, 이 기능은 아래와 같은 컨텍스트를 가지고 있다.

  1. DB 커넥션 가져오기

  2. PreparedStatement 를 만들어줄 외부 기능 호출하기

  3. 전달받은 PreparedStatement 실행하기

  4. 예외가 발생하면 이를 다시 메소드 밖으로 던지기

  5. 모든 경우에 만들어진 PreparedStatement 와 Connection 을 적절하게 닫아주기

여기에서 2번을 제외하고는 사실상 모든 Dao 로직에서 나머지 부분들이 반복된다. 따라서 다음과 같이 인터페이스를 이용하여 바꿔볼 수 있다.

public interface StatementStrategy {
    PreparedStatement makePreparedStatement(Connection var1) throws SQLException;
}

public class DeleteAllStatement implements StatementStrategy {
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        PreparedStatement ps = c.prepareStatement("delete from users");
        return ps;
    }
}

public class UserDao() {
    ...
    public void deleteAll() throws SQLException {
        try{
            c = dataSource.getConnection();
            
            StatementStrategy strategy = new DeleteAllStatement();
            ps = strategy.makePreparedStatement();
        
            ps.executeUpdate();
        } catch (SQLException e){
        ...
        }
    }
}

하지만 이 경우, 또 다시 문제점이 발생한다. 컨텍스트인 deleteAll() 에서 이미 구체적인 전략인 DeleteAllStatement() 를 사용하도록 고정되어있다. 컨텍스트가 인터페이스 이외에 인터페이스의 구현체까지 알고 있는 것은 좀 이상하다.

4. DI 적용을 위한 클라이언트/컨텍스트 분리 🙆

보통 전략패턴에 따르면 아래와 같은 구조가 되어야 한다.

  • 클라이언트는 전략을 선택 및 생성하고, 이러한 전략을 컨텍스트에 제공한다.

  • 클라이언트로부터 전략을 받은 컨텍스트는 전략 인터페이스 내 매소드를 호출한다.

  • 전략 인터페이스의 구현체 매소드가 실행된다.

따라서, UserDao 는 아래와 같은 구조로 리펙토링 되어야 할 것이다.

public class UserDao {
    ...

    //컨텍스트
    public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException {
        Connection c = null;
        PreparedStatement ps = null;

        try {
            c = this.dataSource.getConnection();
            ps = stmt.makePreparedStatement(c);
            ps.executeUpdate();
        } catch (SQLException var15) {
            throw var15;
        } finally {
            if (ps != null) {
                try {
                    ps.close();
                } catch (SQLException var14) {
                }
            }

            if (c != null) {
                try {
                    c.close();
                } catch (SQLException var13) {
                }
            }

        }

    }
    
    ...

    //클라이언트
    public void deleteAll() throws SQLException {
        Statement st = new DeleteAllStatement();
        jdbcContextWithStatementStrategy(st);
    }

    ...
}

JDBC 전략 패턴의 최적화

  • 위까지 리펙토링을 했지만, 그럼에도 불구하고 2가지의 아쉬움이 있다.

    • DAO 로직이 새로 생겨날 때마다, StatementStrategy 구현 클래스를 만들어야 한다. 이렇게 계속 새로 생성되는 것은 로직마다 상속하는 템플릿 메소드 패턴과 다를 것이 없다.

    • 또 다른 점은 add() 와 같은 매소드의 경우는 User 와 같은 부가정보를 넘겨받아 statement 를 실행하기 때문에, 부가적인 생성자를 만들어주어야 한다.

이 문제를 해결하는 방법은 두 가지 방법이 있을 것 같다. 1. 로직클래스와 2. 익명클래스 이다.

로컬 클래스

로컬 클래스는 매소드 내부에 정의된 클래스를 말한다. DeleteAllStatement 나 AddStatement 는 어차피 한번씩 UserDao 에서만 사용된다. 그렇다면 굳이 외부 클래스로 두지 말고 매소드 내부에 클래스 정의를 하자는 입장이다.

로컬 클래스로 바꿨을 경우, 장점은 다음과 같다.

  • 매소드 내부에서 정의되기 때문에 자신이 선언된 곳의 내부 정보에 접근이 가능하다.

    • add() 매소드의 파라미터로 넘어온 user 를 바로 가져다가 쓸 수 있기 때문에 클래스 내부에서 별도의 생성자로 받아 인스턴스 변수로 저장하고 있지 않아도 된다.

    • 다만, 이렇게 로컬 클래스에서 바로 접근하려면 매개변수로 전달된 User 에 final 이 붙어야 한다.

  • 클래스 파일 1개를 줄였고, add() 매소드 내에서 PreparedStatement 생성로직을 함께 볼 수 있다.

public class UserDao {
    ...
    public void add(final User user) throws SQLException {
        
        //add() method 내부에 정의된 AddStatement() 구현체 
        class AddStatement implements StatementStrategy {
        
        public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
            PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values(?,?,?)");
            ps.setString(1, user.getId());
            ps.setString(2, user.getName());
            ps.setString(3, user.getPassword());
            return ps;
        }
    }
}

참고) 중첩클래스의 종류

  1. static class : 독립적으로 오브젝트로 만들어질 수 있음

  2. inner class, 내부클래스 : 자신이 정의된 클래스의 오브젝트 안에서만 만들어질 수 있음

    1. 멤버 내부 클래스 : 멤버 필드처럼 오브젝트 레벨에 정의됨

    2. 로컬 클래스 : 매소드 레벨에 정의됨

    3. 익명 내부 클래스 : 이름을 갖지 않는 익명 내부 클래스. 범위는 선언된 위치에 따라서 다름

      1. 이름이 없기 때문에 클래스 자신의 타입을 가질 수 없다.

      2. 구현한 인터페이스 타입의 변수에만 저장할 수 있다.

익명 내부 클래스

  • 아예 add() 매소드 내부에서만 사용되는 것이라면 이름이 필요 없을수도 있다. 익명클래스로 만들어볼수도 있다.

public class UserDao {
    ...
    public void add(final User user) throws SQLException {
        this.jdbcContextWithStatementStrategy(new StatementStrategy() {
            public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values(?,?,?)");
                ps.setString(1, user.getId());
                ps.setString(2, user.getName());
                ps.setString(3, user.getPassword());
                return ps;
            }
        });
    }

    public void deleteAll() throws SQLException {
        this.jdbcContextWithStatementStrategy(new StatementStrategy() {
            public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                return c.prepareStatement("delete from users");
            }
        });
    }
    
    ...
}

컨텍스트와 DI

  • 지금까지의 흐름을 정리해보자면 아래와 같다.

    • 클라이언트 UserDao

    • 컨텍스트 : jdbcContextWithStatementStrategy()

    • 개별 전략 : 익명 내부 클래스로 만들어진 두 개의 strategy

클래스 분리

  • JDBC 는 일반적으로 UserDAO 뿐만 아니라 다른 DAO에서도 사용이 가능하다. 따라서 별도의 클래스로 분리하여 사용하는 것이 더 나을 것 같다.

public class JdbcContext {
    DataSource dataSource;

    public JdbcContext() {
    }

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public void workWithStatementStrategy(StatementStrategy stmt) throws SQLException {
        Connection c = null;
        PreparedStatement ps = null;

        try {
            c = this.dataSource.getConnection();
            ps = stmt.makePreparedStatement(c);
            ps.executeUpdate();
        } catch (SQLException var15) {
            throw var15;
        } finally {
            if (ps != null) {
                try {
                    ps.close();
                } catch (SQLException var14) {
                }
            }

            if (c != null) {
                try {
                    c.close();
                } catch (SQLException var13) {
                }
            }

        }

    }
}


public class UserDao {
    private JdbcContext jdbcContext;

    public UserDao() {
    }

    public void setDataSource(JdbcContext jdbcContext) {    //JdbcContext 를 DI 받는다. 
        this.jdbcContext = jdbcContext;
    }

    public void add(final User user) throws SQLException {
        this.jdbcContext.workWithStatementStrategy(new StatementStrategy() { ... }
        });
    }
    
    public void deleteAll() throws SQLException {
        this.jdbcContext.workWithStatementStrategy(new StatementStrategy() { ... }
        });
    }
}

빈 의존관계 변경

  • 위와 같이 JdbcContext 를 별도로 분리하게 되면, 의존관계가 다음과 같이 변경된다.

    • UserDao -> JdbcContext(구체) -> DataSource (인터페이스) <- SimpleDriverDS (구현체)

  • 일반적인 DI의 경우, 두 오브젝트가 인터페이스를 사이에 두고 서로 느슨하게 연결되어있는 것이 기본인데, UserDao 와 JdbcContext 의 경우는 좀 특별하다. JdbcContext 는 JdbcContext 를 제공해주는 서비스 오브젝트로서의 의미만 있고, 그 내부의 구현방법이 변경될 가능성이 없다. 따라서 변동성을 염두해두어 인터페이스를 두고 느슨하게 연결할 필요 없이, 그냥 JdbcContext class 구체를 직접 연결해준다.

  • 위의 의존관계 변경에 맞춰, XML 파일 내부의 정보도 변경해준다.

    • 아직까지 UserDao 내에서 JdbcContext 로 변경하지 않은 부분들이 있으니 우선 dataSource 를 주입받고 있도록 설정을 지우지 않았다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://www.springframework.org/schema/beans 
                  http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

   <bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
      ...
   </bean>
   
   <bean id="userDao" class="springbook.user.dao.UserDao">
      <!-- 추후 jdbcContext 로 완전히 전환되면 삭제 예정 -->
      <property name="dataSource" ref="dataSource" />
      <property name="jdbcContext" ref="jdbcContext" />
   </bean>
   
   <!-- 추가된 jdbcContext 타입의 빈 -->
   <bean id="jdbcContext" class="springbook.user.dao.jdbcContext">
      <property name="dataSource" ref="dataSource" />
   </bean>
   
</beans>

JdbcContext 의 특별한 DI

  • JdbcContext 에 DI 할 수 있는 두 가지 방법이 있다.

    • 스프링 빈을 통한 DI

    • 코드를 이용하는 수동 DI

스프링 빈을 통한 DI

UserDao -> JdbcContext 간의 관계가 인터페이스를 이용한 연결이 아니라 구체로 고정되어있다. 이 경우에도 여전히 DI 라고 할 수 있을까?

  • 여전히, jdbcContext 는 DI 라고 말할 수 있는데, DI 개념 자체가 객체의 생성과 관계설정에 대한 제어 권한을 오브젝트에서 제거하고 외부로 위임했다는 IoC 개념을 포괄하기 때문이다. 현재 JdbcContext 는 스프링을 이용하여 UserDao 객체에서 사용하게 주입되고 있기 때문에 DI 의 기본을 따르고 있다고 할 수 있다.

  • 그렇다면 JdbcContext 는 왜 굳이 DI 를 따라야 할까? 두 가지 이유가 있다.

    • 하나는 스프링 컨테이너 내에서 싱글톤 레지스트리에서 관리되는 싱글톤 빈이 될 수 있기 때문이다.

      • JbdcContext 의 경우, 그 자체로 변경되는 상태정보가 없고, dataSource 라는 인스턴스 변수가 있지만, 조회용이므로 큰 상관이 없다.

      • 게다가 JbdcContext 는 JDBC 컨텍스트 메소드를 제공해주는 일종의 서비스 오브젝트로서의 의미가 있기 때문에 싱글톤으로 등록 되어서 여러 오브젝트에서 공유해 사용되는 것이 좋다.

    • 또 다른 하나는 JdbcContext 가 DI 를 통해 다른 빈에 의존하고 있기 때문이다.

      • 스프링 DI 를 위해서는 대상이 되는 오브젝트들이 모두 스프링 컨테이너에 의해 관리를 받는 IoC 대상이어야 한다. 따라서 dataSource 빈을 주입받기 위해서라도 JdbcContext 는 스프링 빈으로 관리가 되어야 한다.

왜 인터페이스를 사용하지 않는가?

  • JdbcContext 의 경우는 UserDao 와 매우 긴밀한 관계를 가지고 있다. UserDao 는 반드시 JdbcContext 와 같이 사용되기 때문이다.

  • JdbcContext 의 경우, Jpa나 하이버네이트와 같은 ORM 을 사용해야한다면 아예 통째로 바뀌어야 한다.

  • dataSource 와 달리, 테스트에서도 다른 구현으로 대체해서 사용할 이유가 없다.

따라서 위와 같은 경우, 별도로 인터페이스를 두지 않고 강력한 결합을 인정하며 스프링 빈으로 등록하여 DI 되도록 만드는 것이 좋다. 싱글톤으로 만들수도 있고 JdbcContext 내부에 사용되는 DI 를 위해서라도 말이다.

다만, 이런 코드 구성은 최후의 고민수단이다. DI 는 일단 인터페이스를 사용하여 관계를 맺는 것을 기본으로 하기 떄문에 이 경우는 매우매우매우 특수한 경우이므로, 주의해서 사용하자. 인터페이스 만들기 귀찮다고 이렇게 쓰는 것은 절대로 아니다...!

코드를 이용하는 수동 DI

  • 스프링 빈으로 등록해서 DI 하지 않는다면, UserDao 내부에서 직접 DI 를 적용하는 방법도 있다.

public class UserDao {
    private DataSource dataSource;
    private JdbcContext jdbcContext;

    public UserDao() {
    }

    public void setDataSource(DataSource dataSource) {    
        //수정자 매소드이지만, JdbcContext 에 대한 DI 작업도 동시에 이루어지고 있다. 
        this.jdbcContext = new JdbcContext();
        this.jdbcContext.setDataSource(dataSource);
        this.dataSource = dataSource;        // 아직 jdbcContext 를 이용하고 있지 않은 매소드를 위해 남겨두는 코드 
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://www.springframework.org/schema/beans 
                  http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
                  
   <bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
      ...
   </bean>
   
   <bean id="userDao" class="springbook.user.dao.UserDao">
      <property name="dataSource" ref="dataSource" />
   </bean> 
</beans>

비교하기 : 스프링 빈을 이용한 DI vs. 코드를 이용한 수동 DI

  • 스프링 빈을 이용한 DI

    • 장점 : 오브젝트 간 의존관계가 설정파일에서 명확하게 드러난다.

    • 단점 : DI 근본 원칙에 부합하지 않는 구체적인 클래스와의 관계가 설정에 직접 노출된다.

  • 코드를 이용한 수동 DI

    • 장점 : UserDao - JdbcContext 간의 관계가 외부에 직접적으로 노출되지 않는다.

    • 단점

      • JdbcContext 를 여러 오브젝트가 사용하더라도 싱글톤으로 만들 수 없다.

      • DI 작업을 위해서 별도로 코드 작업이 필요하다.

장단점은 각자가 알아서 비교해보며 사용하면 된다. 어느 것이 더 낫다고 말할 수 없다. 특정 것을 사용하는데 분명한 이유가 있는 것이 훨씬 중요하다.

템플릿과 콜백

  • 템플릿/콜백 패턴 : 복잡하지만 바뀌지 않는 일정한 패턴을 갖는 작업 흐름이 존재하고, 그 중 일부만 교체하여 사용해야하는 경우, 전략 패턴 기본 구조 + 익명 내부 클래스를 활용하는데, 이러한 방식을 스프링에서는 템플릿/콜백 패턴이라고 부른다.

    • 전략패턴의 컨텍스트를 템플릿, 익명 내부 클래스로 만들어지는 오브젝트를 콜백이라고 부른다.

  • 템플릿

    • 어떤 목적을 위해 미리 만들어둔 모양이 있는 틀

    • 참고) 템플릿 메소드 패턴 : 고정된 틀의 로직을 가진 템플릿 메소드를 슈퍼클래스에 두고, 바뀌는 부분을 서브 클래스의 메소드에 두는 구조

  • 콜백

    • 실행되는 것을 목적으로 다른 오브젝트의 매소드에 전달되는 오브젝트를 말한다.

    • 자바에서는 매소드를 파라미터로 전달할 수 없기 때문에, 메소드가 담인 오브젝트를 전달해야한다.

템플릿/콜백의 특징

  • 단일 메소드 인터페이스를 사용한다. 특정 기능을 사용하기 위해 한번만 호출되는 경우가 일반적이기 떄문이다.

    • 보통 전략 패턴의 경우는 여러 개의 메소드를 가진 인터페이스를 사용할 수 있도록 한다.

  • 콜백은 일반적으로 하나의 매소드를 가진 인터페이스를 구현한 익명 내부 클래스로 만들어진다.

  • 콜백 인터페이스 메소드에는 보통 파라미터가 존재하며 이는 템플릿의 작업 흐름 중에 만들어지는 컨텍스트 정보를 전달받기 위함이다.

  • DI 방식의 전략 패턴 구조라고 이해하면 좋다.

  • 클라이언트 UserDao 가 템플릿 매소드 workWithStatementStrategy() 를 호출하면서 콜백 오브젝트 StatementStragety 구현체를 전달하는 것은 메소드 레벨에서 일어나는 DI 이다. 오브젝트를 생성함과 동시에 주입하고 있는 셈이다.

  • 템플릿/콜백 패턴에서는 매번 메소드 단위로 사용할 오브젝트를 새롭게 전달받는다.

    • 일반적인 DI 라면 템플릿에 인스턴스 변수를 만들어두고 사용할 의존 오브젝트를 수정자 메소드로 받아서 사용한다

  • 콜백 오브젝트가 내부 클래스로서 자신을 생성한 클라이언트 매소드 내의 정보를 직접 참조한다는 것도 특이하다. (final User 등)

콜백의 분리와 재활용

  • 장점

    • 클라이언트인 DAO 메소드가 간결해진다. 최소한의 데이터 엑세스 로직만 가지고 있게 된다.

  • 단점

    • 내부 클래스로 코드가 되어있어 가독성이 좀 떨어진다.

public class UserDao {
    ...
    
    public void deleteAll() throws SQLException {
        executeSql("delete from users");
    }
    
    private void executeSql(final String query) throws SQLException{
        this.jdbcContext.workWithStatementStrategy(
            new StatementStrategy() {
                public PreparedStatement makePreparedStatement(Connection c) 
                        throws SQLException{
                    
                    return c.prepareStatement(query);
                }
            }
        );
    }
}
  • 위와 같이 공통된 부분을 추출하여 다른 DAO 메소드에서도 활용할 수 있도록 하였다. 이제 deleteAll() 에서는 실행할 쿼리문만 전달하면 된다.

콜백과 템플릿의 결합

  • 한단계 더 나아가, UserDao 내부에서만 executeSql() 을 사용할 것이 아니라 외부까지 그 범위를 확장해보는 것은 어떨까. 다른 DAO 클래스들이 공통으로 사용하고 있는 JdbcContext 클래스 내부로 옮겨주자.

public class JdbcContext {
    ...
    private void executeSql(final String query) throws SQLException{
        workWithStatementStrategy(
            new StatementStrategy() {
                public PreparedStatement makePreparedStatement(Connection c) 
                        throws SQLException{
                    
                    return c.prepareStatement(query);
                }
            }
        );
    }
}


public class UserDao {
    ...
    
    public void deleteAll() throws SQLException {
        this.jdbcContext.executeSql("delete from users");
    }
}
  • 이렇게 구조를 바꾸니, 클라이언트인 UserDao 에서는 쿼리문만 전달해주고, 나머지 쿼리 실행을 위한 목적을 공유하는 코드들은 JdbcContext 클래스 내부로 모이게 되었다.

    • 코드의 성격이 다른 것들은 분리하는 것이 보통이지만, 이 경우는 같은 목적을 위해 긴밀하게 움직이고 있으므로 한군데 모여있는 것이 유리하다.

  • 구체적인 구현, 내부 전략패턴, 코드에 의한 DI, 익명 내부 클래스 등 기술은 최대한 감춰졌고, 외부에는 꼭 필요한 기능만 오픈할 수 있게 되었다.

템플릿/콜백의 응용 - Calculator

1. test 만들기

  • 덧셈기능을 가진 계산기를 만들어보자

  • 아래와 같이 한줄에 하나씩 숫자가 존재하고, 각 라인의 숫자를 더해 그 합을 반환해주는 기능을 가진 계산기를 만든다.

//numbers.txt
1
2
3
4
public class CalcSumTest {

    @Test
    public void sumOfNumbers() throws IOException {
        Calculator calculator = new Calculator(); 
        int sum = calculator.calcSum(getClass().getResource("numbers.txt").getPath());
        assertThat(sum, is(10));
    }
}
public class Calculator {
    public Calculator() {
    }

    public Integer calcSum(String filepath) throws IOException {
        BufferedReader br = new BufferedReader(new FileReader(filePath));
        Integer sum = 0;
        String line = null;
        while ((line = br.readLine()) != null) {
            sum += Integer.valueOf(line);
        }
        
        br.close();
        return sum;
    }
}

2. try, catch, finally 로 예외처리하기

public class Calculator { 
    public Calculator() { }
    
    public Integer calcSum(String filepath) throws IOException {
        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader(filePath));
            Integer sum = 0;
            String line = null;
            while ((line = br.readLine()) != null) {
                sum += Integer.valueOf(line);
            }
            return sum;
        
        } catch(IOException e) {
        
        } finally {
            if (br != null) {
                try { br.close(); }
                catch (IOException e) { System.out.println(e.getMessage()); }
            }
        }
    }
}

3. 중복제거 및 템플릿/콜백 설계하기

  • 곱셈기능 및 다른 기능이 추가된다면...? 위처럼 길다란 try ~ catch 문을 무지성으로 반복할수는 없다. 일정한 패턴이 보이므로 패턴을 따보자.

//BufferedReaderCallback.java
public interface BufferedReaderCallback {
    Integer doSomethingWithReader(BufferedReader br) throw IOException;
}

//Calculator.java
public Integer fileReadTemplate(String filePath, BuffredReaderCallback callback) 
        throws IOException {
        
    BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader(filePath));
            int result = callback.doSomethingWithReader(br);
            return result;
        } catch(IOException e) {
        
        } finally {
            if (br != null) {
                try { br.close(); }
                catch (IOException e) { System.out.println(e.getMessage()); }
            }
        }
    
}

//Calculator.java
public class Calculator { 
    ...
    
    public Integer calcSum(String filepath) throws IOException {
        BufferedReaderCallBack callback = new BufferedReaderCallback() {
            public Integer doSomethingWithReader(BufferedReader br) 
                    throw IOException {
                    
                Integer sum = 0;
                String line = null;
                
                while ((line = br.readLine()) != null) {
                    sum += Integer.valueOf(line);
                }
        
                return sum;
            }
        };
        
        return fileReadTemplate(filePath, callback);
    }
    
    public Integer calcMultiply(String filepath) throws IOException {
        BufferedReaderCallBack callback = new BufferedReaderCallback() {
            public Integer doSomethingWithReader(BufferedReader br) 
                    throw IOException {
                    
                Integer sum = 0;
                String line = null;
                
                while ((line = br.readLine()) != null) {
                    sum *= Integer.valueOf(line);
                }
        
                return sum;
            }
        };
        
        return fileReadTemplate(filePath, callback);
    }
}


//CalcSumTest.java
public class CalcSumTest {
    Calculator calculator;
    String numFilepath;

    public CalcSumTest() {
    }

    @Before
    public void setUp() {
        this.calculator = new Calculator();
        this.numFilepath = this.getClass().getResource("numbers.txt").getPath();
    }

    @Test
    public void sumOfNumbers() throws IOException {
        Assert.assertThat(this.calculator.calcSum(this.numFilepath), CoreMatchers.is(10));
    }

    @Test
    public void multiplyOfNumbers() throws IOException {
        Assert.assertThat(this.calculator.calcMultiply(this.numFilepath), CoreMatchers.is(24));
    }
}

4. 템플릿/콜백 재설계

  • 그럼에도 불구하고 반복되는 구조 발견! ->read lines

  • 이 부분도 개선해보자.

//LineCallback.java
public interface LineCallback {
    Integer doSomethinkWithLine(String line, Integer value);
}


//Calculator.java
public class Calculator { 
    ...
    
    public Integer calcSum(String filepath) throws IOException {
        LineCallback callback = new LineCallback() {
            public Integer doSomethinkWithLine(String line, Integer value) 
                    throw IOException {
                return value += Integer.valueOf(line);
            }
        };
        
        return lineReadTemplate(filePath, callback);
    }
    
    public Integer calcMultiply(String filepath) throws IOException {
        LineCallback callback = new LineCallback() {
            public Integer doSomethinkWithLine(String line, Integer value) 
                    throw IOException {
                return value *= Integer.valueOf(line);
            }
        };
        
        return lineReadTemplate(filePath, callback);
    }
    
    
    public Integer lineReadTemplate(String filePath, LineCallback callback, int resultValue) {
        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader(filePath));

            Integer result = resultValue;            
            String line = null;
            while ((line = br.readLine()) != null) {
                result = callback.doSomethinkWithLine(line, result);
            }
            return result;
        
        } catch(IOException e) {
        
        } finally {
            if (br != null) {
                try { br.close(); }
                catch (IOException e) { System.out.println(e.getMessage()); }
            }
        }
    }
}

제네릭스를 이용한 콜백 인터페이스

  • 반환 결과를 Integer 타입이 아닌 String 타입으로 하고 싶다면 어떻게 해야할까?

    • 자바 5부터 추가된 제네릭을 이용하면 된다.

public interface LineCallback<T> {
    T doSomethingWithLine(String line, T value);
}

public <T> T lineReadTemplate(String filePath, LineCallback<T> callback, T initVal) 
        throws Exception {
        
    BufferedReader br = null;
    try {
        br = new BufferedReader(new FileReader(filePath));
        T result = initVal;
        String line = null;
        while ((line = br.readLine()) != null) {
            result = callback.doSomethinkWithLine(line, result);
        }
        
        return result;
    } catch(IOException e) {
        ...
    } finally { ... } 
}
public class CalcSumTest {
    ...
    
    @Test
    public void concatenateStrings() throws IOException {
        Assert.assertThat(this.calculator.concatenate(this.numFilepath), CoreMatchers.is("1234"));
    }
}

public class Calculator {
    ...
    public String concatenate(String filepath) throws IOException {
        LineCallback<String> concatenateCallback = new LineCallback<String>() {
            public String doSomethingWithLine(String line, String value) {
                return value + line;
            }
        };
        return (String)this.lineReadTemplate(filepath, concatenateCallback, "");
    }

    public <T> T lineReadTemplate(String filepath, LineCallback<T> callback, T initVal) throws IOException {
        BufferedReader br = null;

        try {
            br = new BufferedReader(new FileReader(filepath));
            T res = initVal;

            for(String line = null; (line = br.readLine()) != null; res = callback.doSomethingWithLine(line, res)) {
            }

            Object var8 = res;
            return var8;
        } catch (IOException var15) {
            System.out.println(var15.getMessage());
            throw var15;
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException var14) {
                    System.out.println(var14.getMessage());
                }
            }

        }
    }
}

스프링의 JdbcTemplate

public class UserDao {
    private JdbcTemplate jdbcTemplate;
    private RowMapper<User> userMapper = new RowMapper<User>() {
        public User mapRow(ResultSet rs, int rowNum) throws SQLException {
            User user = new User();
            user.setId(rs.getString("id"));
            user.setName(rs.getString("name"));
            user.setPassword(rs.getString("password"));
            return user;
        }
    };

    public UserDao() {
    }

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    public void add(User user) {
        this.jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)", new Object[]{user.getId(), user.getName(), user.getPassword()});
    }

    public User get(String id) {
        return (User)this.jdbcTemplate.queryForObject("select * from users where id = ?", new Object[]{id}, this.userMapper);
    }

    public void deleteAll() {
        this.jdbcTemplate.update("delete from users");
    }

    public int getCount() {
        return this.jdbcTemplate.queryForInt("select count(*) from users");
    }

    public List<User> getAll() {
        return this.jdbcTemplate.query("select * from users order by id", this.userMapper);
    }
}

정리

  • JDBC 와 같은 예외가 발생할 가능성이 있고 공유 리소스의 반환이 필요한 코드는 반드시 try, catch, finally 블록으로 관리해야한다.

  • 일정한 작업 흐름이 반복되면서, 그 중 일부 기능만 바뀌는 코드가 존재한다면 전략 패턴을 적용한다. 바뀌지 않는 부분은 콘텍스트로, 바뀌는 부분은 전략으로 만들고 인터페이스를 통해 유연하게 전략을 젼경할 수 있도록 구성한다.

  • 같은 어플리케이션 안에서 여러가지 종류의 전략을 다이내믹하게 구성하고 사용해야한다면, 컨텍스트를 이용하는 클라이언트 메소드에서 직접 전략을 정의하고 제공하게 만든다.

  • 클라이언트 메소드 안에 익명 내부 클래스를 사용해서 전략 오브젝트를 구현하면 코드도 간결해지고 메소드의 정보를 직접 사용할 수 있어서 편리하다. 컨텍스트가 하나 이상의 클라이언트 오브젝트에서 사용된다면, 클래스를 분리해서 공유하도록 만든다.

  • 컨텍스트는 별도의 빈으로 등록해서 DI 받거나 클라이언트 클래스에서 직접 생성해서 사용한다. 클래스 내부에서 컨텍스트를 사용할 때, 컨텍스트가 의존하는 외부의 오브젝트가 있다면, 코드를 이용해서 직접 DI 해줄수있다.

  • 단일 전략 메소드를 갖는 전략 패턴이면서 익명 내부 클래스를 사용해서 매번 전략을 새로 만들어 사용하고, 컨텍스트 호출과 동시에 전략 DI 를 수행하는 방식을 템플릿/콜백 패턴이라고 한다.

  • 콜백의 코드에도 일정한 패턴이 반복된다면, 콜백을 템플릿에 넣고 재활용하는 것이 편리하다.

  • 템플릿과 콜백의 타입이 다양하게 바뀔 수 있다면 제네릭스를 이용한다.

  • 스프링은 JDBC 코드 작성을 위해 JdbcTemplate 을 기반으로 하는 다양한 템플릿과 콜백을 제공한다.

  • 템플릿은 한번에 하나 이상의 콜백을 사용할 수 있고, 하나의 콜백을 여러 번 호출할 수 있다.

  • 템플릿/콜백을 설계할 때에는 템플릿과 콜백 사이에 주고 받는 정보에 관심을 둬야한다.

☕
토비의 스프링 3.1
출처) 토비의 스프링
출처) 토비의 스프링