📌 동적인 웹 페이지를 생성하기 위해 사용되는 도구이며 템플릿을 기반으로 정적인 부분과 동적인 데이터를 결합하여 HTML, XML 등 의 문서를 생성하는 역할을 수행한다.
우리가 흔히 말하는 UI(User Interface)를 만들며, SSR(Server Side Rendering)에 사용된다.
정적 출력은 [안내 메시지]와 같은 모든 사용자에게 동일하게 보이는 데이터를 말하고 동적 데이터는 [사용자 이름]과 같은 사용자, 상황마다 다른 결과를 표현하는 데이터다.
아래와 같이 서버에서 보내는 데이터를 html쪽에서 정의하는 방식으로 접근이 편리하게 만들어준다.
<!-- 정적 출력 -->
<h1>안녕하세요, 방문해주셔서 감사합니다!</h1>
<!-- 동적 출력 -->
<p>안녕하세요, <span th:text="${name}">이름</span>님!</p>
[실제 출력]
<!-- 정적 출력 -->
<h1>안녕하세요, 방문해주셔서 감사합니다!</h1>
<!-- 동적 출력 -->
<p>안녕하세요, <span>철수</span>님!</p>
템플릿 엔진이 나온 이유
자바 코드로 HTML을 만들어 내는 것이 아닌 HTML 문서에 동적으로 변경해야 하는 부분만 자바 코드를 넣을 수 있다면 더 편리하다.
대표적인 템플릿 엔진
Thymeleaf
Spring과 통합이 잘 되어있다.
다양한 기능을 포함하고 있다.
JSP(Java Server Pages)
예전엔 많이 사용했으나, 현재 안 쓰는 추세
FreeMarker
Velocity
Mustache
MVC 패턴 개요
📌 Servlet이나 JSP만으로 비지니스 로직과 View Rendering 까지 모두 처리하면 너무 많은 역할을 하게 되고 유지보수가 굉장히 어려워져서(책임이 너무 많음) 고안된 패턴이다. Web Application은 일반적으로 MVC(Model View Controller) 패턴을 사용한다.
Servlet 문제점
화면을 그리는 View 영역과 비지니스 로직이 Servlet 하나에 모두 섞여있다.
책임을 너무 많이 가지고 있다.
Servlet 동작 순서
사용자가 Client(브라우저)를 통해 서버에 HTTP Request 즉, API 요청을 한다.
요청을 받은 Servlet 컨테이너는 HttpServletRequest, HttpServletResponse객체를 생성한다.
설정된 정보(URL, HTTP Method)를 통해 어떠한 Servlet에 대한 요청인지 찾는다.
해당 Servlet에서 service 메서드를 호출한 뒤 브라우저의 요청 Method에 따라 doGet() 혹은doPost() 등의 메서드를 호출한다.
서버에서 응답을 생성한 뒤 HttpServletResponse객체에 응답을 담아 Client(브라우저)에 반환한다.
위의 내용을 설명하면, 클라에서 서버에 계속해서 같은 내용을 반복 요청한다는 것, 그런데 그걸 받는 컨트롤러가 하나에만 배치되면 되는데 다 각각 배치된다는 것.. (카페 알바에게 아메리카노 3잔시켰는데 다른 주문도 많은데 알바 3명이서 1개씩 만들고 있다...)
HttpServletResponse 객체를 사용하는 경우가 적다.(JSP에서 모두 해결하기 때문)
HttpServletRequest와 HttpServletResponse는 Test 코드를 작성하기도 매우 힘들다.
공통 기능이 추가될수록 Controller에서 처리해야 하는 부분들이 많아진다.
공통 기능 처리
모든 컨트롤러에서 공통으로 적용되는 기능을 뜻한다.
ex) Log 출력, 인증, 인가 등
공통 기능을 Method로 분리하여 각각의 컨트롤러에서 사용하면 되는것 아닌가요? 라고 생각이 가능하지만, 공통 기능으로 만들어놓은 Method 또한 항상 중복적으로 호출이 필요합니다. 또한, 사람인 개발자가 작업하다보면 Method를 호출하는 일을 깜빡 할수도 있고 Method가 많아지면 많아질수록 Controller의 책임이 점점 커진다. 이를 위해 아래의 대안이 나왔다.
프론트 컨트롤러 패턴
📌 Servlet(Controller)이 호출되기 전에 공통 기능을 하나의 Servlet에서 처리해주는 패턴이다. 프론트 컨트롤러(Servlet) 하나에 모든 클라이언트측 요청이 들어온다.
프론트 컨트롤러의 역할
모든 요청을 하나의 프론트 컨트롤러가 받는다.
공통 기능을 처리한다.
요청을 처리할 수 있는 Controller를 찾아서 호출한다.(Controller Mapping)
프론트 컨트롤러를 제외한 나머지 컨트롤러는 Servlet을 사용하지 않아도 된다.
일반 Controller들은 HttpServlet을 상속( HTTP 프로토콜 기반의 요청과 응답을 처리하기 위해 제공되는 클래스 )받거나, @WebServlet(URL 매핑 여기서는 url과 servlet과의 매핑을 말한다.)을 사용하지 않아도 된다. = 프론트 컨트롤러가 처리함
@WebServlet의 설명이 이해가 안될 수도 있는데, url은 말 그대로 주소 식별자다. url에 보내는 요청을 보고 어느 servlet에서 처리를 할지 정하고 해당 servlet에 배치(매핑)하는 과정은 완전별개의 문제이다.
프론트 컨트롤러 의문점
프론트 컨트롤러를 사용하면 모든 컨트롤러에서 같은 형태의 응답을 해야하는가?
위 그림처럼 공통 처리 로직에 모든 컨트롤러가 연결되기 위해서는 모든 컨트롤러가 return 하는 결과의 형태가 동일해야 한다.
하지만, Controller 마다 로직이나 응답해야하는 결과는 당연히 다를테고 응답을 동일하게 맞추려고 한다면 해당 애플리케이션은 확장성, 유지보수성을 잃는다.
공통 로직에서 응답별로 퍼즐을 다시 하나하나 처리할 수 있으나 공통 부분의 책임이 너무 커지게된다. 또한, 컨트롤러에서 반환되는 결과가 달라지면 공통처리 부분의 변경또한 불가피하다.
그래서 아래의 대안이 또 나왔다.
어댑터 패턴
📌 어댑터 패턴은 호환되지 않는 인터페이스를 가진 클래스들이 함께 동작할 수 있도록 중간에서 변환을 도와주는 디자인 패턴입니다. 주로, 이미 존재하는 코드와 새로운 코드 사이의 호환성 문제를 해결하는 데 사용됩니다.
컨트롤러들은 동일한 인터페이스를 구현하도록 하고 해당 인터페이스와 공통 로직 사이에 어댑터를 두어 유연하게 만든다. 서로 다른 인터페이스를 갖는 두 클래스를 연결해주는 패턴이다.
📌 Java와 유사한 문법 구조를 가진 Groovy기반의 스크립트 언어를 사용하며 다양한 소프트웨어를 빌드(Build)할 수 있는 유연한 빌드 자동화 도구이다.
빌드란 소스 코드를 컴퓨터가 실행 가능한 파일로 변환해주는 작업이다.
빌드와 빌드 자동화 도구
[1] Gradle 특징
유연성
복잡한 빌드 시나리오를 처리할 수 있는 유연한 시스템을 제공한다.
빌드 스크립트를 통해 다양한 빌드 작업을 정의하고, 필요한 경우 커스터마이징할 수 있다.
성능
Build Cache
빌드 결과물을 캐싱하여 재사용한다.
라이브러리 의존성을 캐싱하여 재사용한다.
점진적 빌드
마지막 빌드 호출 이후 변경된 부분만 빌드한다.
변경되지 않은 부분은 캐시 결과를 검색해 재사용한다.
데몬 프로세스
다음 빌드 작업을 위해 백그라운드에서 대기하는 프로세스
초기 빌드 이후부터는 빌드 실행 시 초기화 작업을 거치지 않는다.
멀티 프로젝트 빌드 지원
공통으로 사용하는 클래스를 모듈로 만들어 독립적인 각 프로젝트에서 사용할 수 있도록 한다.
설정 주입 방식
필요한 설정을 직접 프로젝트에 주입하는 방식이다.
공통되는 정보는 묶어서 한번에 주입이 가능하다.
프로젝트별로 설정을 다르게 주입할 수 있다.
[2] Gradle과 다른 빌드 도구 비교
Gradle
Maven
Ant
설정 방식
Groovy/Kotlin DSL
XML
XML
성능
빠름 (증분 빌드, 캐싱 지원)
비교적 느림
느림 (모든 작업을 매번 실행)
의존성 관리
지원 (Maven, Ivy 호환)
지원 (Maven Central)
제한적 (Ivy 필요)
확장성
매우 유연, 사용자 정의 플러그인 가능
제한적
매우 유연
주요 사용 사례
현대적 프로젝트, 멀티 플랫폼 지원
Java 프로젝트 중심
간단한 자동화 작업
[3] 주요 Gradle 명령어
gradle tasks
사용 가능한 모든 작업(task) 목록을 표시.
gradle build
프로젝트를 빌드(컴파일, 테스트, 패키징 등)합니다.
gradle clean
이전 빌드 결과를 삭제합니다.
gradle test
프로젝트의 테스트를 실행합니다.
gradle run
애플리케이션 실행 (특정 플러그인이 필요).
gradle dependencies
프로젝트의 의존성을 확인합니다.
gradle assemble
소스 코드를 컴파일하고 JAR 파일 또는 기타 배포 가능한 아티팩트를 생성합니다.
[4] Gradle에서 의존성 관리
Gradle은 의존성 관리를 통해 프로젝트의 외부 라이브러리를 다운로드하고 관리합니다. 의존성은 dependencies 블록 안에서 정의하며, 저장소는 repositories 블록에서 설정합니다.
1. 의존성 추가
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web' // 실행 시 필요한 라이브러리
testImplementation 'org.junit.jupiter:junit-jupiter' // 테스트 용도 라이브러리
}
2. 저장소 설정
repositories {
mavenCentral() // Maven Central Repository
jcenter() // JCenter Repository
}
[5] Gradle 멀티 프로젝트 설정
Gradle은 대규모 애플리케이션에서 멀티 프로젝트 빌드를 지원합니다.
루트 프로젝트의 settings.gradle
rootProject.name = 'my-multi-project'
include 'module1', 'module2' // 하위 프로젝트 포함
각 하위 프로젝트의 build.gradle
plugins {
id 'java'
}
dependencies {
implementation project(':module1') // 다른 모듈을 의존성으로 추가
}
[6] Gradle의 장점
효율적인 빌드 속도:
Gradle의 증분 빌드 기능은 수정된 부분만 빌드하여 속도를 대폭 향상시킵니다.
유연한 설정:
Groovy 또는 Kotlin DSL을 사용하여 빌드 스크립트를 자유롭게 작성 가능하며, 복잡한 프로젝트도 쉽게 관리 가능합니다.
강력한 플러그인 시스템:
수많은 플러그인이 제공되며, 사용자 정의 플러그인을 만들어 프로젝트에 통합할 수 있습니다.
확장성:
다양한 언어 및 플랫폼 지원으로 다양한 종류의 애플리케이션 빌드 가능.
커뮤니티 및 문서화:
활발한 커뮤니티와 풍부한 공식 문서로 개발자가 쉽게 문제를 해결할 수 있습니다.
[7] Gradle의 주요 단점
초기 학습 곡선
Gradle은 유연성과 DSL(Groovy/Kotlin) 기반의 스크립트를 제공하지만, 설정이 복잡할 경우 학습 곡선이 가파릅니다.
느린 초기 빌드
증분 빌드나 빌드 캐시를 사용하지 않는 초기 빌드는 Maven보다 느리게 느껴질 수 있습니다.
모호한 에러 메시지
복잡한 빌드 스크립트에서 문제가 발생할 경우, 에러 메시지가 직관적이지 않거나 원인을 파악하기 어려운 경우가 있습니다.
DSL 의존성
Groovy 또는 Kotlin DSL을 학습해야 하며, 기존 XML 기반(Maven 등)에 익숙한 사용자에게는 진입 장벽이 될 수 있습니다.
플러그인 의존성 문제
플러그인의 호환성 문제가 발생하거나 최신 플러그인을 찾기 어려운 경우가 종종 있습니다.
의존성 관리 중복 가능성
프로젝트 구조가 복잡해질수록 의존성 충돌 문제가 발생할 가능성이 있으며, 이를 해결하기 위해 추가적인 설정이 필요합니다.
멀티 프로젝트 빌드 관리 어려움
멀티 프로젝트 설정이 유연한 대신 복잡한 빌드 환경에서는 디버깅 및 설정 관리가 까다로울 수 있습니다.
Maven보다 적은 레퍼런스
Maven에 비해 더 적은 사용자와 레퍼런스를 제공하므로, 특정 문제에 대한 해결 정보를 찾기 어려울 때가 있습니다.
build.gradle
📌 Groovy 기반 언어의 빌드 스크립트로 스크립트를 작성하면 소스 코드를 빌드하고, 라이브러리들의 의존성을 관리할 수 있다.
Gradle 프로젝트의 빌드 설정과 의존성 관리를 담당하는 핵심 구성 파일입니다. Groovy DSL을 사용하여 작성되며, Gradle 프로젝트의 다양한 동작을 정의
// 1. 플러그인 정의
plugins {
id 'java' // Java 플러그인
id 'org.springframework.boot' version '3.0.0' // Spring Boot 플러그인
}
// 2. 프로젝트 정보
group = 'com.example' // 그룹 ID
version = '1.0.0' // 버전 정보
sourceCompatibility = '17' // Java 버전
// 3. 저장소 설정
repositories {
mavenCentral() // Maven Central 저장소
}
// 4. 의존성 관리
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web' // Spring Boot Starter Web
testImplementation 'org.springframework.boot:spring-boot-starter-test' // 테스트 의존성
}
// 5. 태스크 정의 (선택적)
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8' // UTF-8 인코딩 설정
}
[1] 주요 구성 요소
1. 플러그인
Gradle은 플러그인을 통해 프로젝트의 기능을 확장합니다.
plugins {
id 'java' // Java 애플리케이션 빌드용
id 'org.springframework.boot' version '3.0.0' // Spring Boot 프로젝트 설정용
id 'io.spring.dependency-management' version '1.1.0' // Spring 의존성 관리
}
2. 프로젝트 정보
프로젝트의 기본 정보와 Java 버전 등을 설정합니다.
group = 'com.example' // 조직명
version = '1.0.0' // 애플리케이션 버전
sourceCompatibility = '17' // Java 버전
3. 저장소
외부 라이브러리를 가져오는 저장소를 지정합니다.
repositories {
mavenCentral() // Maven Central Repository
jcenter() // JCenter Repository (필요 시 추가)
}
Spring Boot는 application.properties 또는 application.yml 파일을 사용해 간단하게 애플리케이션의 설정을 관리할 수 있습니다.
서버의 자동 실행
내장 웹 서버를 통해 별도의 설정 없이, 애플리케이션을 java -jar 명령어로 실행할 수 있습니다.
개발자 친화적인 환경
Spring Boot는 애플리케이션의 개발과 디버깅을 빠르게 할 수 있도록 DevTools라는 기능을 제공하며, 코드 변경 시 자동으로 애플리케이션을 재시작하여 개발 편의성을 제공합니다.
[2] Spring Boot의 장점
빠른 시작:
Spring Boot는 자동 설정 및 기본적인 템플릿을 제공하여, 애플리케이션을 몇 가지 설정만으로 빠르게 시작할 수 있습니다. @SpringBootApplication 어노테이션을 추가하면, 기본적인 설정이 자동으로 이루어지고, 애플리케이션을 실행할 수 있는 상태가 됩니다.
설정 최소화:
Spring Boot는 많은 설정을 자동으로 처리하므로 개발자는 비즈니스 로직에만 집중할 수 있습니다. 예를 들어, 데이터베이스 설정, 서버 설정 등 대부분의 설정을 자동으로 처리하여 개발자가 별도로 신경 쓸 필요가 없습니다.
내장 서버:
내장 서버를 제공하여 별도의 외부 웹 서버(Tomcat, Jetty 등)를 설치할 필요 없이 바로 실행할 수 있습니다. 이는 애플리케이션의 배포를 간소화하고, 실행 파일 하나로 애플리케이션을 배포할 수 있게 만듭니다.
생산성 향상:
Spring Boot는 개발자가 빠르게 애플리케이션을 작성하고 실행할 수 있도록 돕기 위해 많은 스타터 의존성을 제공합니다. 예를 들어, 웹 애플리케이션을 만들기 위한 spring-boot-starter-web, 데이터베이스 연동을 위한 spring-boot-starter-data-jpa 등이 있습니다.
DevTools:
Spring Boot는 개발 중에 자동 재시작 및 Hot swapping을 지원하여, 개발자가 코드 수정 후 애플리케이션을 다시 시작하지 않고도 변경 사항을 바로 반영할 수 있도록 돕습니다.
[3] Spring Boot 애플리케이션 구조
애플리케이션 클래스
@SpringBootApplication 어노테이션이 붙은 클래스는 Spring Boot 애플리케이션의 진입점입니다. 이 클래스는 자동으로 필요한 설정을 수행하고 애플리케이션을 실행합니다.
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
자동 설정:
Spring Boot는 애플리케이션을 실행할 때, 주어진 환경에 맞춰 자동으로 설정을 적용합니다. 예를 들어, 데이터베이스가 설정되면 자동으로 DataSource와 관련된 설정이 적용됩니다.
프로퍼티 파일:
Spring Boot는 application.properties 또는 application.yml 파일을 통해 애플리케이션의 설정을 관리할 수 있습니다. 데이터베이스 연결 정보, 서버 포트, 로깅 수준 등 다양한 설정을 이 파일에서 처리합니다.
내장 서버:
Spring Boot는 기본적으로 내장된 Tomcat 서버를 포함하고 있으며, 설정에 따라 다른 내장 서버(Undertow, Jetty 등)를 사용할 수 있습니다.
[4] Spring Boot 애플리케이션 실행
Maven 또는 Gradle을 사용한 빌드 후 실행:
애플리케이션을 빌드하고 실행하려면, mvn spring-boot:run 또는 gradle bootRun 명령을 사용할 수 있습니다.
JAR 파일로 실행:
Spring Boot 애플리케이션을 JAR 파일로 빌드한 후, java -jar 명령어로 실행할 수 있습니다.
java -jar myapp.jar
[5] Spring Boot 사용 예시
간단한 RESTful 웹 서비스:
@RestController
@RequestMapping("/api")
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello, World!";
}
}
자동 설정 예시: Spring Boot는 데이터베이스 연결을 자동으로 처리할 수 있습니다. 예를 들어, 데이터베이스 설정을 application.properties에서 지정하면, Spring Boot는 이를 자동으로 인식하고 설정을 완료합니다.
📌 Java Application Framework로 엔터프라이즈 애플리케이션 개발에 주로 사용된다.
엔터프라이즈 애플리케이션은 대규모로 복잡한 비즈니스 프로세스와 데이터를 처리하는 애플리케이션을 뜻한다.
IoC (Inversion of Control), AOP (Aspect-Oriented Programming), DI (Dependency Injection) 등의 개념을 바탕으로 개발되었으며, 이를 통해 애플리케이션의 구조를 유연하고 모듈화하여 유지보수성을 높이고, 복잡한 엔터프라이즈 애플리케이션 개발을 더 간단하게 만들어 줍니다.
Spring Framework로 만드는 Web Application 라면 : Java 냄비 : Spring
[1] Spring Framework 특징
애플리케이션의 다양한 구성 요소를 유연하게 연결하고 관리할 수 있도록 해준다.
Spring Framework는 누구나 사용할 수 있는 오픈소스 이다.
모듈화되어 있어 필요에 따라 특정 기능만 선택적으로 사용할 수 있다.
Java언어의 가장 큰 특징인 객체 지향 언어의 특징을 살려낸 프레임워크이다.
캡슐화
상속
추상화
다형성
[2] Spring의 구조
Spring Framework는 여러 모듈로 구성되어 있으며, 각 모듈은 특정 목적에 맞는 기능을 제공합니다. 주요 모듈은 다음과 같습니다:
Core Container:
Core: Spring의 핵심 기능인 IoC (Inversion of Control) 및 DI (Dependency Injection)를 제공.
Beans: 객체 생성 및 관리.
Context: Spring의 애플리케이션 컨텍스트 기능을 제공, 이벤트 관리와 같은 다양한 기능 포함.
Spring AOP: AOP 기능을 제공하여, 메서드 호출 전후의 동작을 정의.
Data Access/Integration:
JDBC: 데이터베이스와의 상호작용을 쉽게 할 수 있도록 지원.
ORM: Hibernate, JPA 등 객체-관계 매핑을 처리.
JMS: Java 메시지 서비스 (JMS)를 지원하여 메시지 기반 애플리케이션 구현.
Web:
Spring MVC: 웹 애플리케이션을 위한 모델-뷰-컨트롤러 패턴을 지원.
WebSocket: 실시간 통신을 위한 WebSocket 지원.
WebFlux: 반응형 프로그래밍 모델을 기반으로 한 웹 프레임워크.
Security:
인증, 권한 부여 및 다양한 보안 관련 기능을 제공합니다.
Testing:
Spring Test 모듈은 Spring 애플리케이션을 테스트하는 데 필요한 다양한 유틸리티를 제공합니다. @SpringBootTest 등을 통해 통합 테스트를 쉽게 할 수 있습니다.
[3] Spring Framework의 주요 특징
1. IoC (Inversion of Control) / DI (Dependency Injection)
Spring은 객체를 직접 생성하는 대신, IoC 컨테이너를 통해 객체를 관리합니다. 객체 간의 의존 관계를 DI 방식으로 해결하여, 애플리케이션의 결합도를 낮추고 유연성을 높입니다.
예를 들어, 하나의 클래스가 다른 클래스에 의존할 때, 이를 자동으로 주입해주는 방식입니다.
@Component
public class Service {
private final Repository repository;
@Autowired // Repository를 자동으로 주입
public Service(Repository repository) {
this.repository = repository;
}
}
2. AOP (Aspect-Oriented Programming)
AOP는 관점 지향 프로그래밍으로, 핵심 비즈니스 로직과 공통 기능(로깅, 트랜잭션 관리 등)을 분리하여 개발할 수 있게 도와줍니다. 이로 인해 코드가 더 깔끔해지고 재사용성이 증가합니다.
Spring에서는 AOP를 이용해 메서드 호출 전후에 특정 처리를 추가할 수 있습니다.
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Method called: " + joinPoint.getSignature().getName());
}
}
3. 모듈화
Spring은 여러 개의 모듈로 구성되어 있어 필요한 기능만 선택적으로 사용할 수 있습니다. 예를 들어, Spring Data, Spring Security, Spring Web 등 다양한 서브모듈을 통해 애플리케이션의 요구에 맞는 기능을 추가할 수 있습니다.
4. 트랜잭션 관리
Spring은 선언적 트랜잭션 관리 기능을 제공하여, 비즈니스 로직에서 트랜잭션을 쉽게 관리할 수 있도록 도와줍니다. @Transactional 어노테이션을 사용해 메서드 또는 클래스 수준에서 트랜잭션을 자동으로 관리할 수 있습니다.
@Transactional
public void transferMoney(Account fromAccount, Account toAccount, double amount) {
// 돈 이체 처리 로직
}
5. Spring MVC
Spring MVC는 웹 애플리케이션을 위한 모델-뷰-컨트롤러(MVC) 프레임워크입니다. 요청을 처리하고, 뷰를 렌더링하며, 사용자 인터페이스를 구축하는 데 필요한 기능을 제공합니다.
Spring MVC는 RESTful API 개발도 지원하여, REST 기반의 웹 서비스를 쉽게 구축할 수 있습니다.
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello, World!";
}
}
6. Spring Boot
Spring Boot는 Spring Framework의 복잡성을 줄여주는 자동 설정과 내장 서버를 제공합니다. 이를 통해 설정을 최소화하고, 독립 실행형 애플리케이션을 쉽게 만들 수 있습니다. Spring Boot는 Spring Cloud와 결합하여 마이크로서비스 아키텍처를 쉽게 구현할 수 있습니다.
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
7. Spring Security
Spring Security는 인증(Authentication)과 권한 부여(Authorization)를 처리하는 프레임워크로, 웹 애플리케이션에 필요한 보안 기능을 제공합니다. 로그인, 로그아웃, CSRF 방어, 권한 체크 등의 기능을 손쉽게 설정할 수 있습니다.
8. Spring Data
Spring Data는 데이터베이스와의 상호작용을 단순화하는 데 도움을 줍니다. 특히 JPA(Java Persistence API)를 사용하여 관계형 데이터베이스와의 통합을 돕고, CRUD 작업을 자동화합니다.
📌 코드에 메타데이터를 추가할 수 있는 기능을 제공하며 주로 코드에 특별한 의미를 부여하거나, 컴파일러와 런타임에 특정 동작을 트리거하기 위해 사용된다.
어노테이션은 코드에서 직접적인 로직 실행에 영향을 미치지 않지만, 코드의 의미를 설명하거나 추가적인 처리를 위해 사용됩니다.
"명함"를 생각하면 편하다. 이 명함은 "사람"에서 "프로그래머인 사람"이 된다. 사람이라는 정체성은 그대로이지만, 이 사람의 용도를 알 수 있다. 코드의 용도를 표시하며 실제로 컴파일러도 그 의미를 알지만 프로그램(사람) 자체에는 변화가 없다.
[1] 어노테이션 정의
어노테이션은 @ 기호로 시작하며, 클래스, 메서드, 변수, 매개변수, 패키지 등에 추가할 수 있다.
[2] 내장 어노테이션
@Override
메서드가 상위 클래스나 인터페이스의 메서드를 오버라이드하고 있음을 나타낸다.
이때 컴파일러는 메서드가 실제로 오버라이드하고 있는지 확인한다.
@Deprecated
해당 요소가 더 이상 사용되지 않음을 나타낸다.
해당 어노테이션이 붙은 코드를 사용하면 컴파일 경고가 발생한다.
@SuppressWarnings
컴파일러 경고를 억제한다.
사용되지 않는 변수에 대한 경고를 무시할 수 있다.
[3] 사용자 정의 어노테이션
개발자가 필요에 따라 직접 어노테이션을 정의할 수 있다.
사용자 정의 어노테이션은 특정 메타데이터를 추가하거나,
AOP(Aspect-Oriented Programming) 같은 기술과 결합하여 다양한 기능을 구현할 수 있다.
AOP는 심화 주차에 배울 내용
Lombok
📌 Java에서 반복적인 코드를 줄여주는 라이브러리로, 코드의 가독성과 유지보수성을 높이는 데 도움이 됩니다. 주로 getter, setter, toString, equals, hashCode 메서드와 같은 보일러플레이트 코드를 자동으로 생성해줍니다.
[1] Lombok 사용 시 장점:
보일러플레이트 코드 감소: Lombok은 반복적인 코드(예: getter, setter, 생성자 등)를 자동으로 생성해 주어 코드가 간결해집니다.
코드의 가독성 향상: 중요한 로직에 집중할 수 있어 코드가 더 깔끔하고 이해하기 쉬워집니다.
생산성 향상: 반복적인 코드 작성에 소모되는 시간을 절약할 수 있습니다.
[2] Lombok 사용 시단점:
자동 생성된 코드의 가시성 부족: Lombok은 컴파일 시 코드 생성이 이루어지기 때문에, IDE에서 코드가 어떻게 처리되는지 확인하기 어려운 경우가 있습니다.
디버깅 어려움: Lombok이 생성한 코드는 실제로 파일에 존재하지 않기 때문에, 디버깅 시 자동으로 생성된 메서드를 추적하는 데 어려움이 있을 수 있습니다.
@Getter, @Setter
클래스의 모든 필드에 대한 getter와 setter 메서드를 자동으로 생성한다.
예시 코드
@Getter
@Setter
public class User {
private String name;
private int age;
/** 아래 코드를 @Getter, @Setter 어노테이션이 생성해준다.
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
**/
}
위 코드에서 getName(), setName(String name), getAge(), setAge(int age) 메서드가 자동으로 생성된다.
@ToString
객체의 toString() 메서드를 자동으로 생성한다.
기본적으로 클래스의 모든 필드를 포함하며, 특정 필드를 제외하거나 포맷을 지정할 수도 있다.
예시코드
@ToString
public class User {
private String name;
private int age;
}
toString() 메서드는 객체를 String으로 변환해주는 역할을 수행한다.
@EqualsAndHashCode
equals()와 hashCode() 메서드를 자동으로 생성한다.
객체의 동일성과 해시 코드를 정의하는데 사용된다.
예시 코드
@EqualsAndHashCode
public class User {
private String name;
private int age;
}
📌 프레임워크는 특정 프로그래밍 작업을 수행하기 위한 기반 구조를 제공하는 도구입니다. 예를 들어, 웹 애플리케이션 개발을 위한 Spring이나 Django와 같은 프레임워크는 애플리케이션 아키텍처와 구조를 정의하고, 개발자가 해당 구조 내에서 작업할 수 있도록 도와줍니다.
프레임워크는 frame(틀) work(일하다)의 합성어로 일하기 위한 틀을 제공한다. 개발자는 해당 틀에서 일을 해야 한다.
라이브러리가 도화지라면 프레임워크는 채색북과 같다.둘다 그림을 완성시키는 도구이지만, 도화지는 완전히 자유로운 디자인을 할 수 있고 채색북은 자유롭지는 못하지만 편하게 그림을 완성시킬 수 있다.
[1] 프레임워크의 주요 특징:
구조 제공: 프레임워크는 애플리케이션 개발의 기본 뼈대를 제공합니다. 예를 들어, 어떤 파일을 어디에 두고, 어떻게 코드를 구성할지에 대한 규칙을 제시합니다.
규칙과 흐름: 프레임워크는 개발자가 따를 일정한 흐름을 정의합니다. 즉, 개발자가 애플리케이션을 어떻게 구조화할지에 대한 가이드라인을 제공합니다.
확장성: 프레임워크는 애플리케이션을 개발하는 데 있어 기능을 추가하거나 수정할 수 있는 방법을 제공합니다. 그러나 기본적으로는 프레임워크 내에서 정해진 규칙을 따라야 합니다.
재사용성: 프레임워크는 많은 기능을 미리 구현해두어, 개발자는 이러한 기능을 재사용할 수 있습니다. 예를 들어, 데이터베이스 연결, 보안 관리, 사용자 인증 등이 미리 구현되어 있는 경우가 많습니다.
[2] 프레임워크의 예시:
웹 개발 프레임워크:
Spring Framework (Java): 웹 애플리케이션을 만들 때 필요한 기본 구조와 규칙을 제공합니다. Spring은 의존성 주입, AOP, 보안, 데이터베이스 연동 등 여러 기능을 제공합니다.
Django (Python): Python으로 웹 애플리케이션을 개발할 때 사용하는 프레임워크로, 기본적인 웹 애플리케이션의 구조와 URL 처리, 데이터베이스 연동 등의 기능을 제공합니다.
Ruby on Rails (Ruby): Ruby 언어로 웹 애플리케이션을 빠르게 개발할 수 있도록 도와주는 프레임워크입니다. RESTful 방식의 API 설계와 모델-뷰-컨트롤러(MVC) 아키텍처를 따릅니다.
모바일 앱 개발 프레임워크:
React Native: JavaScript를 사용하여 iOS와 Android에서 실행되는 네이티브 앱을 개발할 수 있게 해주는 프레임워크입니다.
Flutter: Google에서 만든 프레임워크로, Dart 언어를 사용해 크로스 플랫폼 애플리케이션을 개발할 수 있습니다.
[3] 장점
개발 프로젝트에 일관된 구조를 제공하여 코드의 일관성과 가독성을 높여주며 팀 협업이 편해진다.
기본적으로 필요한 기능과 도구를 제공하여 개발자들이 핵심 비즈니스 로직에 집중할 수 있다.
보안 관련 기능을 기본적으로 제공하여, 보안 취약점을 방지하는 데 도움을 준다.
통합된 테스트 환경과 도구를 제공하여 테스트를 쉽게 작성하고 실행할 수 있다.
인기 있는 프레임워크는 방대한 커뮤니티 지원을 받으며, 다양한 문서를 활용할 수 있다.
[4] 단점
프레임워크는 굉장히 복잡한 구조를 가지기 때문에, 처음 익히는 데 시간이 많이 소요된다.
프레임워크의 새로운 버전이 기존 코드와 호환되지 않을 수 있다.
정해진 규칙과 구조를 따르게 강제하여 자유롭게 변경하기 어려울 수 있다.
Library
📌 특정 기능을 수행하는 코드의 모음으로, 개발자가 필요할 때 그 기능을 호출하여 사용할 수 있는 도구입니다. 라이브러리는 애플리케이션의 흐름을 제어하지 않으며, 개발자가 원하는 기능만 골라서 사용할 수 있습니다. 즉, 라이브러리는 필요한 도구를 제공하지만, 애플리케이션의 전체적인 흐름은 개발자가 주도합니다.
[1] 라이브러리의 주요 특징:
선택적 사용: 라이브러리는 개발자가 필요한 기능을 원할 때 호출해서 사용할 수 있습니다. 개발자가 전체적인 흐름을 제어하고, 필요한 기능만 사용할 수 있습니다.
재사용성: 특정 기능을 여러 번 사용할 수 있도록 기능을 모듈화하여 제공합니다. 예를 들어, 수학 계산, 문자열 처리, HTTP 요청 보내기 등의 기능을 쉽게 사용할 수 있습니다.
독립적: 라이브러리는 보통 독립적으로 동작하며, 다른 라이브러리나 애플리케이션에 종속되지 않습니다.
[2] 라이브러리의 예시:
JavaScript 라이브러리:
jQuery: HTML 문서를 다루고, 이벤트를 처리하며, AJAX 요청을 보내는 등의 기능을 쉽게 사용할 수 있도록 도와주는 JavaScript 라이브러리입니다. jQuery를 사용하면 DOM 조작을 더 간단히 할 수 있습니다.
Lodash: 배열, 객체, 함수 등의 데이터를 쉽게 다룰 수 있는 JavaScript 유틸리티 라이브러리입니다. 반복적인 코드 작성을 줄여주고, 다양한 유틸리티 함수들을 제공합니다.
Python 라이브러리:
NumPy: 수치 계산을 위한 Python 라이브러리로, 다차원 배열 및 행렬 연산을 지원하여 과학적 계산을 손쉽게 할 수 있습니다.
Pandas: 데이터를 다루고 분석하는 데 유용한 라이브러리로, 데이터 프레임(DataFrame) 구조를 사용하여 데이터 처리 및 분석을 쉽게 합니다.
Java 라이브러리:
Apache Commons: 다양한 유틸리티 기능을 제공하는 Java 라이브러리로, 문자열 처리, 파일 입출력 등 여러 가지 기능을 간편하게 사용할 수 있습니다.
Google Guava: Java에서 컬렉션, 캐시, 문자열 처리 등을 편리하게 처리할 수 있도록 돕는 라이브러리입니다.
[3] 라이브러리 사용의 장점:
빠른 개발: 이미 구현된 기능을 가져다 쓸 수 있어 개발 속도가 빨라집니다.
모듈화: 필요한 기능만 가져다 쓰므로 코드가 깔끔하고 모듈화가 잘 됩니다.
다양한 기능: 특정 작업을 처리하는 다양한 라이브러리가 존재하여, 원하는 기능을 쉽게 찾아 사용할 수 있습니다.
[4] 라이브러리 사용의 단점:
라이브러리가 업데이트 되거나 지원이 중단될 경우 문제가 발생할 수 있다.
버전 호환성 문제로 인해 다른 라이브러리나 기존 코드와 충돌이 발생할 수 있습니다.
생각보다 빈번하게 발생하는 문제
불필요한 기능을 포함한 라이브러리를 사용하면 비효율적이다.
라이브러리의 내부 구현을 직접 수정하기 어려워, 특정 요구 사항에 맞게 조정하기 힘들 수 있다.
라이브러리와 프레임워크의 차이점을 표로 정리하면 다음과 같습니다:
라이브러리
프레임워크
제어의 흐름
개발자가 흐름을 제어하고, 필요한 기능을 호출
프레임워크가 흐름을 제어하고, 개발자는 그 안에서 작업
사용 방식
필요한 기능을 선택하여 사용
전체적인 구조를 따르고, 규칙에 맞춰 작업
의존성
독립적이며, 필요할 때마다 호출하여 사용
애플리케이션 구조에 강하게 의존
구조 제공 여부
특정 기능을 제공하지만, 전체 구조는 제공하지 않음
전체적인 애플리케이션 구조를 제공
사용 예
특정 기능만 필요할 때 (예: jQuery, Lodash)
애플리케이션 개발 시 기본적인 구조가 필요한 경우 (예: Spring, Django)
개발자 역할
개발자가 원하는 기능을 필요에 맞게 선택하고 적용
개발자는 프레임워크의 규칙에 따라 작업
예시
jQuery, NumPy, Google Maps API
Spring, Django, Ruby on Rails
요약:
라이브러리는 개발자가 필요한 기능을 선택하여 사용하는 도구이고, 흐름 제어는 개발자에게 있습니다.
프레임워크는 애플리케이션의 구조와 흐름을 제공하며 라이브러리를 이미 포함하고 있습니다. 흐름 제어는 프레임워크가 담당합니다.
📌 클린 코드는 단순히 잘 동작하는 코드가 아니라, 가독성, 유지보수성, 확장성이 뛰어난 코드를 의미합니다. 이는 협업과 장기적인 코드 품질을 유지하기 위한 필수적인 개발 철학 입니다.
Dirty Code는 폭탄이다. A를 수정하니 B가 터지는 연쇄반응이 수시로 나온다.
협업의 필수 조건으로, 사실 본인이 혼자해도 이 규칙을 어느정도 지켜야한다.
기술 부채가 쌓이면 나중에는 결국 리펙토링이 아니라 재개발하는게 더 좋을 수도 있다.
프로그래머는 가뜩이나 이직이 많아 에일리언코드(담당자가 없어져서든 버전의 변화로든 알 수 없는 코드를 말한다.)가 많이 생성될 가능이 높다.
감당 못함
리팩터링
📌 결과의 변경 없이 코드의 구조를 재조정하는 것을 말합니다. 주로 가독성을 높이고 유지보수를 편하게 하기 위해 추후 수정하는 것을 말합니다. 버그를 없애거나 새로운 기능을 추가하는 행위는 아닙니다.
코드가 읽기 어려운 순간 (코드를 읽으면 바로 이해가 되어야한다.)
수정할 때마다 오류가 발생하는 경우
같은 코드가 여기저기 복붙되어 있는 경우
확장하려는데 코드 구조가 방해되는 경우
단위 테스트 작성이 어려운 경우
보통 위와 같은 상황에서 진행하게 된다.
클린 코드의 기본 원칙
의미 있는 이름 짓기
구체적이고 의도를 담은 이름을 사용
매직 넘버를 피하라
데이터의 의미를 이름에 반영
함수 분리 하기
하나의 함수는 하나의 역할만 수행 (sendEmail은 이메일 발송의 흐름만 관리)
복잡한 작업은 작은 함수로 분리
함수 이름은 동작과 목적을 명확히 표현
불필요한 주석 제거
주석은 코드가 아닌 의도를 설명
“어떻게”가 아닌 “왜”를 설명
주석 대신 명확한 변수와 함수 이름으로 의도를 드러냄
불필요한 주석은 제거하고, 코드는 가능한 자체적으로 읽히게 작성
코드 중복 제거
DRY (Don’t Repeat Yourself) 원칙을 준수
복잡한 코드를 단순화하기
조건문이 복잡하거나 여러 논리를 포함한다면 메서드로 분리
부정 표현을 긍정 표현으로 바꾸기
긍정적 변수명 사용
긍정적 조건문 작성
이중 부정 지양
else 문 사용 지양하기
else 문을 피하고 기본 동작을 명시
전처리와 핵심 로직을 분리
각 조건은 독립적으로 처리
의미 있는 이름 짓기
// 나쁜 예
public void processData(List<String> data) {
for (String item : data) {
if (item.length() > 5) {
System.out.println(item);
}
}
}
// 좋은 예
public void printLongUserNames(List<String> userNames) {
final int MIN_NAME_LENGTH = 5;
for (String userName : userNames) {
if (userName.length() > MIN_NAME_LENGTH) {
System.out.println(userName);
}
}
}
구체적이고 의도를 담은 이름을 사용
매직 넘버를 피하라
데이터의 의미를 이름에 반영
함수 분리 하기
// 나쁜 예
public void sendEmail(String recipient, String subject, String body) {
if (recipient == null || recipient.isEmpty()) {
throw new IllegalArgumentException("Recipient cannot be null or empty");
}
System.out.println("Connecting to SMTP server...");
System.out.println("Authenticating...");
System.out.println("Sending email to: " + recipient);
System.out.println("Subject: " + subject);
System.out.println("Body: " + body);
System.out.println("Email sent successfully.");
}
// 좋은 예
public void sendEmail(String recipient, String subject, String body) {
validateRecipient(recipient);
connectToSmtpServer();
authenticate();
deliverEmail(recipient, subject, body);
}
private void validateRecipient(String recipient) {
if (recipient == null || recipient.isEmpty()) {
throw new IllegalArgumentException("Recipient cannot be null or empty");
}
}
private void connectToSmtpServer() {
System.out.println("Connecting to SMTP server...");
}
private void authenticate() {
System.out.println("Authenticating...");
}
private void deliverEmail(String recipient, String subject, String body) {
System.out.println("Sending email to: " + recipient);
System.out.println("Subject: " + subject);
System.out.println("Body: " + body);
System.out.println("Email sent successfully.");
}
★ 하나의 함수는 하나의 역할만 수행 (sendEmail은 이메일 발송의 흐름만 관리)
복잡한 작업은 작은 함수로 분리
함수 이름은 동작과 목적을 명확히 표현
불필요한 주석 제거
// 나쁜 예
public void processTransaction(Account fromAccount, Account toAccount, double amount) {
// 송금 금액이 0보다 커야 합니다.
if (amount <= 0) {
throw new IllegalArgumentException("송금 금액은 0보다 커야 합니다.");
}
// 잔액 확인
if (fromAccount.getBalance() < amount) {
throw new IllegalStateException("계좌 잔액이 부족합니다.");
}
// 같은 계좌인지 확인
if (fromAccount.equals(toAccount)) {
throw new IllegalArgumentException("같은 계좌로 송금할 수 없습니다.");
}
// 송금 실행
fromAccount.withdraw(amount); // 돈을 출금합니다.
toAccount.deposit(amount); // 돈을 입금합니다.
// 송금 로그
System.out.println("송금 성공: " + amount + "원 전송됨.");
}
// 좋은 예
public void processTransaction(Account fromAccount, Account toAccount, double amount) {
// 비즈니스 규칙: 송금 금액은 0보다 커야 함
if (amount <= 0) {
throw new IllegalArgumentException("송금 금액은 0보다 커야 합니다.");
}
// 비즈니스 규칙: 송금 계좌 잔액이 부족하면 송금 불가
if (fromAccount.getBalance() < amount) {
throw new IllegalStateException("계좌 잔액이 부족합니다.");
}
// 비즈니스 규칙: 동일 계좌 간 송금 금지 (실수 방지 목적)
if (fromAccount.equals(toAccount)) {
throw new IllegalArgumentException("같은 계좌로 송금할 수 없습니다.");
}
// 송금 실행
fromAccount.withdraw(amount);
toAccount.deposit(amount);
// 로그 기록: 성공적인 송금을 기록 (보안 및 추적 목적)
System.out.println("송금 성공: " + amount + "원 전송됨.");
}
주석은 코드가 아닌의도를 설명
“어떻게”가 아닌 “왜”를 설명
주석 대신명확한 변수와 함수 이름으로 의도를 드러냄
불필요한 주석은 제거하고, 코드는 가능한자체적으로 읽히게 작성
사실 주석은 함부로 쓰지 않는게 좋다.추후 관리가 어렵고 주석은 전부 개발자가 직접 달아야한다보니 더 문제가 많이 생긴다.
코드 중복 제거
// 나쁜 예
public void printUserName(String name) {
System.out.println("User: " + name);
}
public void printAdminName(String name) {
System.out.println("Admin: " + name);
}
// 좋은 예
public void printName(String role, String name) {
System.out.println(role + ": " + name);
}
DRY (Don’t Repeat Yourself)원칙을 준수
복잡한 코드를 단순화하기
// 나쁜 예
if (user != null && user.getAge() > 18 && user.isActive()) {
// do something
}
// 좋은 예
if (isActiveAdultUser(user)) {
// do something
}
private boolean isActiveAdultUser(User user) {
return user != null && user.getAge() > 18 && user.isActive();
}
조건문이 복잡하거나 여러 논리를 포함한다면 메서드로 분리
isActiveAdultUser가 중요한 것 = 왜 했는지
부정 표현을 긍정 표현으로 바꾸기
// 나쁜 예
if (!user.isInActive()) {
return "Inactive User";
}
return "Active User";
// 좋은 예
if (user.isActive()) {
return "Active User";
}
return "Inactive User";
긍정적 변수명 사용 - 이거 상당히 중요하다
긍정적 조건문 작성
이중 부정 지양 = 이건 반의어를 생각해봐라, 진짜 웬만해서는 피해야한다.
isinactive 라면 긍정인 isactive로 수정한다. 부정은 결국 한번 더 생각을 해야한다..
else 문 사용 지양하기
// 나쁜 예
public void login(User user) {
if (user != null) {
if (user.isActive()) {
if (user.isVerified()) {
System.out.println("Login successful");
} else {
System.out.println("User is not verified");
}
} else {
System.out.println("User is inactive");
}
} else {
System.out.println("Invalid user");
}
}
// 좋은 예
public void login(User user) {
if (user == null) {
System.out.println("Invalid user");
return;
}
if (!user.isActive()) {
System.out.println("User is inactive");
return;
}
if (!user.isVerified()) {
System.out.println("User is not verified");
return;
}
// 모든 조건을 통과한 경우
System.out.println("Login successful");
}
// 더 좋은 예
public void login(User user) {
String validationResult = validateUser(user);
if (!validationResult.equals("Valid")) {
System.out.println(validationResult);
return;
}
// 모든 조건을 통과한 경우
System.out.println("Login successful");
}
private String validateUser(User user) {
if (user == null) {
return "Invalid user";
}
if (!user.isActive()) {
return "User is inactive";
}
if (!user.isVerified()) {
return "User is not verified";
}
return "Valid";
}
else문을 피하고 기본 동작을 명시
전처리와 핵심 로직을 분리
각 조건은 독립적으로 처리
클린 코드 이론 원칙
클린 코드를 작성하는 데 있어 추가적으로 고려할 수 있는 원칙과 실천 사항은 다음과 같습니다:
1. 코드의 가독성 향상
일관성 유지: 코드 스타일(들여쓰기, 괄호 배치, 공백 등)을 일관되게 유지합니다.
짧은 함수: 함수는 가능한 짧게 유지하여 한눈에 이해할 수 있도록 합니다.
코드 정렬: 논리적 흐름을 고려하여 코드를 정렬합니다(예: 관련된 부분끼리 묶기).
2. 객체 지향 설계 원칙(SOLID)
S - 단일 책임 원칙(Single Responsibility Principle) 클래스나 모듈은 하나의 책임만 가져야 합니다.
O - 개방-폐쇄 원칙(Open/Closed Principle) 코드는 확장에는 열려 있고, 수정에는 닫혀 있어야 합니다.
L - 리스코프 치환 원칙(Liskov Substitution Principle) 서브클래스는 언제나 기반 클래스의 역할을 대체할 수 있어야 합니다.
I - 인터페이스 분리 원칙(Interface Segregation Principle) 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 합니다.
D - 의존성 역전 원칙(Dependency Inversion Principle) 상위 모듈은 하위 모듈에 의존해서는 안 됩니다. 둘 다 추상화에 의존해야 합니다.
3. 에러 처리
명확한 예외 처리: 예외를 포괄적으로 잡기보다는 특정한 경우에만 잡아야 합니다.
사전 조건 체크: 함수나 메서드의 입력값 검증을 통해 에러를 방지합니다.
null 사용 지양: null 대신 Optional, Maybe 같은 명시적 표현을 사용합니다.
4. 의존성 관리
의존성 주입(DI): 객체 생성과 사용을 분리하여 의존성을 줄입니다.
전역 상태 지양: 전역 변수를 사용하지 않고 명시적으로 데이터를 전달합니다.
5. 성능 및 최적화
불필요한 작업 최소화: 반복 작업이나 연산을 줄이고 캐싱을 활용합니다.
지연 계산(lazy evaluation): 필요할 때만 계산하도록 설계합니다.
효율적인 자료구조 선택: 목적에 맞는 자료구조를 사용하여 성능을 개선합니다.
6. 테스트 가능 코드
단위 테스트 작성: 각 함수와 모듈의 동작을 검증합니다.
테스트 자동화: CI/CD 환경에서 자동화된 테스트를 실행합니다.
모의 객체(Mock Object) 활용: 외부 의존성을 분리하여 테스트를 단순화합니다.
7. 유지보수성을 고려한 설계
코드 중복 제거: 유틸리티 함수나 상수로 공통된 로직을 추출합니다.
확장성 있는 설계: 변경 사항이 최소한의 영향으로 처리되도록 설계합니다.
의도를 드러내는 코드: 코드만 읽어도 설계 의도를 알 수 있게 작성합니다.
8. 클린 아키텍처
계층 분리: 비즈니스 로직, 프레젠테이션, 데이터 접근 계층을 분리합니다.
엔티티 독립성: 엔티티는 프레임워크나 데이터베이스에 의존하지 않도록 설계합니다.
경계(Interface) 설정: 외부 의존성과 내부 로직 사이에 명확한 경계를 만듭니다.
9. 적절한 캡슐화
정보 은닉: 클래스 내부 상태를 외부에서 직접 접근하지 못하도록 합니다.
Getter/Setter 최소화: 단순히 데이터를 노출하는 Getter/Setter보다는 행동 중심의 메서드 사용.
10. 명확한 의사소통
코드 리뷰: 동료와의 코드 리뷰를 통해 더 나은 코드를 작성합니다.
컨벤션 문서화: 팀에서 사용하는 코딩 스타일을 문서로 명시합니다.
개방-폐쇄 원칙 (Open-Closed Principle)
📌 객체 지향 설계의 핵심 원칙 중 하나로, 소프트웨어 모듈, 클래스, 또는 함수가 다음과 같은 두 가지 상태를 만족해야 한다는 개념
확장에는 열려 있어야 한다 (Open for extension):
새로운 기능을 추가하거나 요구사항의 변화에 따라 시스템의 동작을 확장할 수 있어야 한다.
기존 코드를 수정하지 않고도 기능 추가가 가능해야 한다.
수정에는 닫혀 있어야 한다 (Closed for modification):
기존의 잘 검증된 코드는 수정되지 않아야 한다.
코드 변경이 없기 때문에 기존 시스템의 안정성을 유지할 수 있다.
목표
코드를 재사용 가능하고 유지보수성이 높은 상태로 유지.
새로운 요구사항이 생기더라도 기존 코드의 변경 없이 대응 가능.
class Notification {
public void send(String type) {
if (type.equals("Email")) {
// 이메일 전송 코드
} else if (type.equals("SMS")) {
// SMS 전송 코드
}
}
}
--------------------------------------------
interface Notification {
void send();
}
class EmailNotification implements Notification {
public void send() {
// 이메일 전송 코드
}
}
class SMSNotification implements Notification {
public void send() {
// SMS 전송 코드
}
}
class PushNotification implements Notification {
public void send() {
// 푸시 알림 전송 코드
}
}
class NotificationService {
public void sendNotification(Notification notification) {
notification.send();
}
}
새로운 알림 방식(예: 푸시 알림)을 추가하려면 send 메서드에 조건문을 추가해야 하므로 기존 코드를 수정해야 함.
=> 새로운 알림 방식을 추가하려면 새로운 클래스를 구현하기만 하면 된다. 기존 코드는 수정하지 않는다.
OOP (Object-Oriented Programming)
📌 객체 지향 프로그래밍(OOP)은 현실 세계의 사물이나 개념을 객체(Object)로 모델링하여 소프트웨어를 개발하는 방식
객체를 사용해 데이터를 묶고, 이를 다루는 기능을 추가합니다.
[1] 캡슐화 (Encapsulation)
내부 데이터를 보호하고, 외부에서 접근할 방법을 제한하는 것.
약병은 뚜껑으로 약을 보호하지만, 우리가 약을 꺼낼 수 있는 기능(뚜껑 열기)을 제공해요.
[2] 상속 (Inheritance)
부모가 가진 것을 자식이 물려받는 것.
부모가 물려준 성격(코드)을 자식이 사용하거나 더 발전시킬 수 있어요!
[3] 다형성 (Polymorphism)
같은 동작을 다양한 방식으로 처리할 수 있는 것.
“달려!“라는 명령에 강아지는 뛰고, 자동차는 굴러가요. 하지만 둘 다 “달리는 동작”이에요!
[4] 추상화 (Abstraction)
복잡한 내부 내용은 숨기고, 필요한 부분만 드러내는 것.
자동차의 내부 작동 방식은 몰라도, 운전대와 페달로 운전할 수 있잖아요!
SOLID 원칙 - 면접에서 자주 나온다
📌 OOP를 더 잘 설계하기 위한 5가지 규칙.
쉽게 고치고, 확장할 수 있는 코드를 만드는 지침.
[1] S - 단일 책임 원칙
클래스는 하나의 역할만 가져야 한다.
주방은 요리만, 침실은 잠만. (하나의 역할만!)
왜?
역할이 여러 개면 고칠 때 어디를 수정해야 할지 복잡해 짐.
public class User {
private String name; // 사용자 정보
public void login() { /* 로그인 기능 */ }
public void saveUser() { /* 데이터베이스 저장 기능 */ }
}
--------------------------
public class User { /* 사용자 정보 관리 */ }
public class AuthService {
public void login(User user) { /* 로그인 기능 */ }
}
public class UserRepository {
public void saveUser(User user) { /* 데이터베이스 저장 */ }
}
[2] O - 개방/폐쇄 원칙
코드는 확장에 열려 있고, 수정에는 닫혀 있어야 한다.
옥상에 방을 추가해도 기존 방은 그대로. (확장 가능!)
왜?
기존 코드를 수정하면 예기치 못한 문제가 생길 수 있음.
원래라면 새로운 도형이 추가될 때마다 AreaCalculator 클래스를 수정해야 한다. 하지만 개방 폐쇄 원칙을 적용하면 새로운 도형이 추가되더라도 shape 인터페이스만 구현하면 된다.
다형성을 활용하여 해결한다.
인터페이스를 implements 하여 구현한 새로운 클래스를 만들어서 새로운 기능을 구현한다.
역할(도형)과 구현(원, 사각형, 삼각형 등)을 분리하면 된다.
public class Shape {
public String type;
}
public class AreaCalculator {
public double calculate(Shape shape) {
if (shape.type.equals("circle")) {
return /* 원의 넓이 계산 */;
} else if (shape.type.equals("square")) {
return /* 사각형의 넓이 계산 */;
}
}
}
------------------------
public interface Shape {
double calculateArea();
}
public class Circle implements Shape {
public double calculateArea() { return /* 원의 넓이 계산 */; }
}
public class Square implements Shape {
public double calculateArea() { return /* 사각형의 넓이 계산 */; }
}
public class AreaCalculator {
public double calculate(Shape shape) {
return shape.calculateArea();
}
}
문제점
// Circle을 계산하는 경우
public class Main {
public static void main(String[]) {
AreaCalculator areaCalculator = new AreaCalculator();
Circle circle = new Circle();
areaCalculator.calculate(circle);
}
}
// Square를 계산하는 경우
public class Main {
public static void main(String[]) {
AreaCalculator areaCalculator = new AreaCalculator();
// Circle circle = new Circle();
Square square = new Square();
areaCalculator.calculate(square);
}
}
구현 객체를 변경하기 위해서는 해당 코드를 사용하는 클라이언트측의 코드를 변경해야 한다.
객체의 생성, 사용 등을 자동으로 설정해주는 무엇인가가 필요하다.
Spring Container의 역할
[3] L - 리스코프 치환 원칙
자식 클래스는 부모 클래스를 대체할 수 있어야 한다.
새가 날 수 있는 것처럼, 모든 새는 ‘날기’ 동작을 가져야 한다. (일관성 유지!)
부모 클래스를 사용하는 곳에서 자식 클래스를 사용해도 프로그램의 동작에 문제가 없어야 한다.
왜?
부모 클래스처럼 동작하지 않으면, 프로그램이 예외를 일으킴.
예시
ElectricCar는 Car 클래스를 상속 받았지만, accelerate() 를 사용할 수 없다. LSP 위반
리스코프 치환 원칙 적용
인터페이스를 구현한 구현체를 믿고 사용할 수 있도록 만들어준다.
엑셀은 앞으로 가는 기능이다. 만약 뒤로 간다면 LSP를 위반한다.
class Car {
public void accelerate() {
System.out.println("자동차가 휘발유로 가속합니다.");
}
}
class ElectricCar extends Car {
@Override
public void accelerate() {
throw new UnsupportedOperationException("전기차는 이 방식으로 가속하지 않습니다.");
}
}
public class Main {
public static void main(String[] args) {
Car car = new Car();
car.accelerate(); // "자동차가 가속합니다."
Car electricCar = new ElectricCar();
electricCar.accelerate(); // UnsupportedOperationException 발생
}
}
---------------------------------
// 가속 기능(역할)을 인터페이스로 분리
interface Acceleratable {
void accelerate();
}
class Car implements Acceleratable {
@Override
public void accelerate() {
System.out.println("내연기관 자동차가 가속합니다.");
}
}
class ElectricCar implements Acceleratable {
@Override
public void accelerate() {
System.out.println("전기차가 배터리로 가속합니다.");
}
}
public class Main {
public static void main(String[] args) {
Acceleratable car = new Car();
car.accelerate(); // "내연기관 자동차가 가속합니다."
Acceleratable electricCar = new ElectricCar();
electricCar.accelerate(); // "전기차가 배터리로 가속합니다."
}
}
[4] I - 인터페이스 분리 원칙
클래스는 필요 없는 기능을 강요받지 말아야 한다.
로봇은 먹지 않는데, ‘먹기’ 기능을 구현해야 한다면 비 효율적.
왜?
쓰지 않는 기능 때문에 코드가 불필요하게 복잡해 짐.
특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.
즉, 하나의 큰 인터페이스보다는 여러 개의 작은 인터페이스로 분리해야 한다.
public interface Animal {
void fly();
void run();
void swim();
}
public class Dog implements Animal {
public void fly() { /* 사용하지 않음 */ }
public void run() { /* 달리기 */ }
public void swim() { /* 수영 */ }
}
------------------------------
public interface Runnable {
void run();
}
public interface Swimmable {
void swim();
}
public class Dog implements Runnable, Swimmable {
public void run() { /* 달리기 */ }
public void swim() { /* 수영 */ }
}
[5] D - 의존 역전 원칙
“구체적인 것보다, 추상적인 것에 의존하라.”
예: 모든 가전제품은 콘센트만 꽂으면 작동. (표준 인터페이스!)
왜?
변경에 유연해지고, 재사용성이 높아짐.
예시
NotificationService는 EmailNotifier 클래스를 의존한다.
// Email 알림 클래스
class EmailNotifier {
public void sendEmail(String message) {
System.out.println("Email 알림: " + message);
}
}
// 알림 시스템
class NotificationService {
private EmailNotifier emailNotifier;
public NotificationService() {
// 구체적인 클래스인 EmailNotifier에 의존
this.emailNotifier = new EmailNotifier();
}
public void sendNotification(String message) {
emailNotifier.sendEmail(message);
}
}
public class Main {
public static void main(String[] args) {
NotificationService service = new NotificationService();
service.sendNotification("안녕하세요! 이메일 알림입니다.");
}
}
[이메일 알림이 아닌 SMS 알림과 같은 기능이 추가되면 NotificationService 는 수정되어야 한다. DIP 위반]
-----------------------------------
// 알림 인터페이스(추상화)
interface Notifier {
void send(String message);
}
// Email 알림 클래스
class EmailNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("Email 알림: " + message);
}
}
// SMS 알림 클래스
class SMSNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("SMS 알림: " + message);
}
}
// 알림 서비스 (높은 수준 모듈)
class NotificationService {
// 추상화된 인터페이스에 의존
private Notifier notifier;
// 의존성 주입 (생성자를 통해 주입)
public NotificationService(Notifier notifier) {
this.notifier = notifier;
}
public void sendNotification(String message) {
// notifier가 어떤 구현체인지 상관하지 않음
notifier.send(message);
}
}
public class Main {
public static void main(String[] args) {
// Email 알림을 사용
Notifier emailNotifier = new EmailNotifier();
NotificationService emailService = new NotificationService(emailNotifier);
emailService.sendNotification("안녕하세요! 이메일 알림입니다.");
// SMS 알림을 사용
Notifier smsNotifier = new SMSNotifier();
NotificationService smsService = new NotificationService(smsNotifier);
smsService.sendNotification("안녕하세요! SMS 알림입니다.");
}
}
추상화된 Notifier 인터페이스에만 의존한다.
새로운 알림 방식이 추가되어도 NotificationService 는 변경되지 않아도 된다.
필요한 Notifier 객체를 외부에서 주입받는다.
NotificationService는 어떤 알림 방식을 사용할지에 대한 세부 사항을 몰라도 되므로, 의존성이 약해진다.
모듈간의 결합도를 낮추고 유연성과 확장성을 높일 수 있다.
서로의 변경 사항에 독립적이어서 변경에 유연하다.
의존 역전 원칙 (Dependency Inversion Principle, DIP)
📌객체 지향 설계의 5가지 원칙(SOLID) 중 하나로, 상위 수준의 모듈(비즈니스 로직이나 주요 흐름을 결정하는 코드)과 하위 수준의 모듈(구체적인 구현 코드) 간의 의존성을 분리하고, 둘 다 추상화에 의존하도록 만드는 원칙입니다.
= 말이 어렵지만, 추상화=공통의 기능,주제는 따로 분리하는 것 / 의존성=특정 불변의 기능 혹은 상수에 의해 코드가 유연성이 떨어지는것
= 코드는 구체적인 클래스를 구성하기 전, 공통의 기능을 가진 요소에 대해 인터페이스로 미리 추상화를 거쳐야, 추후 응용이나 변형이 쉽다는 것, 더 쉽게 말하면 그냥 추상화를 잘 유념해라