From 8a91d325c17bed3df2e1e7443c33d1f115c0e939 Mon Sep 17 00:00:00 2001 From: rinarina0429 Date: Tue, 8 Sep 2026 23:23:54 +0900 Subject: [PATCH 01/14] =?UTF-8?q?init:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=B4=88=EA=B8=B0=20=ED=99=98=EA=B2=BD=20=EA=B5=AC?= =?UTF-8?q?=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Java 21 및 Jar 패키징 설정 - Spring Web 의존성 추가 - 프로젝트 메타데이터(com.ceos24.springboot) 설정 --- .gitignore | 43 +++ HELP.md | 22 ++ build.gradle | 28 ++ gradle/wrapper/gradle-wrapper.properties | 9 + gradlew | 248 ++++++++++++++++++ gradlew.bat | 82 ++++++ settings.gradle | 1 + .../com/ceos24/springboot/Application.java | 13 + src/main/resources/application.yaml | 3 + .../ceos24/springboot/ApplicationTests.java | 13 + 10 files changed, 462 insertions(+) create mode 100644 .gitignore create mode 100644 HELP.md create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/com/ceos24/springboot/Application.java create mode 100644 src/main/resources/application.yaml create mode 100644 src/test/java/com/ceos24/springboot/ApplicationTests.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d70d5a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +*# +*.iml +*.ipr +*.iws +*.jar +*.sw? +*~ +.#* +.*.md.html +.DS_Store +.attach_pid* +.classpath +.factorypath +.gradle +.metadata +.project +.recommenders +.settings +.springBeans +.vscode +/code +MANIFEST.MF +_site/ +activemq-data +bin +build +!/**/src/**/bin +!/**/src/**/build +build.log +dependency-reduced-pom.xml +dump.rdb +interpolated*.xml +lib/ +manifest.yml +out +overridedb.* +target +.flattened-pom.xml +secrets.yml +.gradletasknamecache +.sts4-cache + +.idea \ No newline at end of file diff --git a/HELP.md b/HELP.md new file mode 100644 index 0000000..4b7889c --- /dev/null +++ b/HELP.md @@ -0,0 +1,22 @@ +# Getting Started + +### Reference Documentation +For further reference, please consider the following sections: + +* [Official Gradle documentation](https://docs.gradle.org) +* [Spring Boot Gradle Plugin Reference Guide](https://docs.spring.io/spring-boot/4.1.1/gradle-plugin) +* [Create an OCI image](https://docs.spring.io/spring-boot/4.1.1/gradle-plugin/packaging-oci-image.html) +* [Spring Web](https://docs.spring.io/spring-boot/4.1.1/reference/web/servlet.html) + +### Guides +The following guides illustrate how to use some features concretely: + +* [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/) +* [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/) +* [Building REST services with Spring](https://spring.io/guides/tutorials/rest/) + +### Additional Links +These additional references should also help you: + +* [Gradle Build Scans – insights for your project's build](https://scans.gradle.com#gradle) + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..69442c6 --- /dev/null +++ b/build.gradle @@ -0,0 +1,28 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '4.1.1' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'com.ceos24' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-webmvc' + testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ad7845b --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..249efbb --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..8508ef6 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..19b5204 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'spring-boot' diff --git a/src/main/java/com/ceos24/springboot/Application.java b/src/main/java/com/ceos24/springboot/Application.java new file mode 100644 index 0000000..5edd6ed --- /dev/null +++ b/src/main/java/com/ceos24/springboot/Application.java @@ -0,0 +1,13 @@ +package com.ceos24.springboot; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000..f3cad2b --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,3 @@ +spring: + application: + name: spring-boot diff --git a/src/test/java/com/ceos24/springboot/ApplicationTests.java b/src/test/java/com/ceos24/springboot/ApplicationTests.java new file mode 100644 index 0000000..8285b1a --- /dev/null +++ b/src/test/java/com/ceos24/springboot/ApplicationTests.java @@ -0,0 +1,13 @@ +package com.ceos24.springboot; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ApplicationTests { + + @Test + void contextLoads() { + } + +} From 57fabd113cf89ab5d17a7f91053d8c06bc636b8a Mon Sep 17 00:00:00 2001 From: rinarina0429 Date: Tue, 8 Sep 2026 23:58:11 +0900 Subject: [PATCH 02/14] =?UTF-8?q?feat:=20=EB=A3=A8=ED=8A=B8=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C(/)=20Hello=20Spring=20Boot=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/ceos24/springboot/HelloController.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/main/java/com/ceos24/springboot/HelloController.java diff --git a/src/main/java/com/ceos24/springboot/HelloController.java b/src/main/java/com/ceos24/springboot/HelloController.java new file mode 100644 index 0000000..407a473 --- /dev/null +++ b/src/main/java/com/ceos24/springboot/HelloController.java @@ -0,0 +1,13 @@ +package com.ceos24.springboot; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HelloController { + + @GetMapping + public String hello() { + return "Hello, Spring Boot!"; + } +} From e9d465f8dbd3320d37bce36ea300b4575d568381 Mon Sep 17 00:00:00 2001 From: rinarina0429 Date: Tue, 8 Sep 2026 23:58:57 +0900 Subject: [PATCH 03/14] =?UTF-8?q?chore:=20=EC=95=A0=ED=94=8C=EB=A6=AC?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=85=98=20=EC=8B=A4=ED=96=89=20=EC=8B=9C=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=EB=90=9C=20Spring=20Bean=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=B6=9C=EB=A0=A5=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ceos24/springboot/Application.java | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/ceos24/springboot/Application.java b/src/main/java/com/ceos24/springboot/Application.java index 5edd6ed..3567515 100644 --- a/src/main/java/com/ceos24/springboot/Application.java +++ b/src/main/java/com/ceos24/springboot/Application.java @@ -1,13 +1,31 @@ package com.ceos24.springboot; +import java.util.Arrays; +import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; @SpringBootApplication public class Application { - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + public CommandLineRunner commandLineRunner(ApplicationContext ctx) { + return args -> { + + System.out.println("Let's inspect the beans provided by Spring Boot:"); + + String[] beanNames = ctx.getBeanDefinitionNames(); + Arrays.sort(beanNames); + for (String beanName : beanNames) { + System.out.println(beanName); + } + }; + } } From e44e15eef79f051857ee453565ddc737fb34af20 Mon Sep 17 00:00:00 2001 From: rinarina0429 Date: Wed, 9 Sep 2026 00:08:16 +0900 Subject: [PATCH 04/14] =?UTF-8?q?build:=20Spring=20Boot=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - build.gradle에 spring-boot-starter-test 의존성 추가 --- build.gradle | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/build.gradle b/build.gradle index 69442c6..8ef0091 100644 --- a/build.gradle +++ b/build.gradle @@ -1,28 +1,29 @@ plugins { - id 'java' - id 'org.springframework.boot' version '4.1.1' - id 'io.spring.dependency-management' version '1.1.7' + id 'java' + id 'org.springframework.boot' version '4.1.1' + id 'io.spring.dependency-management' version '1.1.7' } group = 'com.ceos24' version = '0.0.1-SNAPSHOT' java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-webmvc' - testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'org.springframework.boot:spring-boot-starter-webmvc' + testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation('org.springframework.boot:spring-boot-starter-test') } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } From a9fea21b6c858cd86f24ded3e637aebee8ada0df Mon Sep 17 00:00:00 2001 From: rinarina0429 Date: Wed, 9 Sep 2026 14:46:38 +0900 Subject: [PATCH 05/14] =?UTF-8?q?test:=20HelloController=20=EB=A3=A8?= =?UTF-8?q?=ED=8A=B8=20API=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MockMvc를 활용한 GET "/" 엔드포인트 테스트 작성 - HTTP 200 OK 상태 코드 및 "Hello, Spring Boot" 응답 문자열 검증 --- .../springboot/HelloControllerTest.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/test/java/com/ceos24/springboot/HelloControllerTest.java diff --git a/src/test/java/com/ceos24/springboot/HelloControllerTest.java b/src/test/java/com/ceos24/springboot/HelloControllerTest.java new file mode 100644 index 0000000..af4911f --- /dev/null +++ b/src/test/java/com/ceos24/springboot/HelloControllerTest.java @@ -0,0 +1,30 @@ +package com.ceos24.springboot; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.test.web.servlet.MockMvc; + +@SpringBootTest +@AutoConfigureMockMvc +public class HelloControllerTest { + + @Autowired + private MockMvc mvc; + + @DisplayName("HelloController의 hello() 메서드 테스트") + @Test + public void getHello() throws Exception { + mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(result -> { + String response = result.getResponse().getContentAsString(); + assert response.equals("Hello, Spring Boot!"); + }); + } +} From 630631da9a2ee149a7c01ca88c47dca8588373c5 Mon Sep 17 00:00:00 2001 From: rinarina0429 Date: Wed, 9 Sep 2026 14:50:14 +0900 Subject: [PATCH 06/14] =?UTF-8?q?build:=20JPA,=20MySQL,=20Lombok=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 데이터베이스 연동을 위한 Spring Data JPA 및 MySQL Connector-J 추가 - 보일러플레이트 코드 제거를 위한 Lombok 의존성 추가 --- build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/build.gradle b/build.gradle index 8ef0091..5c5e8f4 100644 --- a/build.gradle +++ b/build.gradle @@ -21,6 +21,12 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-webmvc' testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + runtimeOnly 'com.mysql:mysql-connector-j' + testImplementation('org.springframework.boot:spring-boot-starter-test') } From 6ff34b0ebbf5c12d1e4a006b92d22666c77d86b3 Mon Sep 17 00:00:00 2001 From: rinarina0429 Date: Wed, 9 Sep 2026 16:05:51 +0900 Subject: [PATCH 07/14] =?UTF-8?q?chore:=20MySQL=20DB=20=EC=97=B0=EB=8F=99?= =?UTF-8?q?=20=EB=B0=8F=20JPA=20=EA=B4=80=EB=A0=A8=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MySQL Datasource 연결 정보 설정 - Hibernate ddl-auto 속성을 create로 설정 - SQL 쿼리 확인을 위한 포맷팅 및 로깅(debug 레벨) 설정 추가 --- src/main/resources/application.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index f3cad2b..50d07fc 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,3 +1,21 @@ spring: application: name: spring-boot + + datasource: + url: jdbc:mysql://localhost:3306/test_db?allowPublicKeyRetrieval=true&useSSL=false&characterEncoding=UTF-8 + username: root + password: ${MYSQL_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + + jpa: + hibernate: + ddl-auto: create + show-sql: true + properties: + hibernate: + format_sql: true + +logging: + level: + org.hibernate.SQL: debug \ No newline at end of file From 4b48b3370ede68480a7f90990c7edeacfb0e0e65 Mon Sep 17 00:00:00 2001 From: rinarina0429 Date: Wed, 9 Sep 2026 16:06:23 +0900 Subject: [PATCH 08/14] =?UTF-8?q?feat:=20Test=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/ceos24/springboot/Test.java | 14 +++++++++++++ .../com/ceos24/springboot/TestController.java | 20 +++++++++++++++++++ .../com/ceos24/springboot/TestRepository.java | 6 ++++++ .../com/ceos24/springboot/TestService.java | 18 +++++++++++++++++ 4 files changed, 58 insertions(+) create mode 100644 src/main/java/com/ceos24/springboot/Test.java create mode 100644 src/main/java/com/ceos24/springboot/TestController.java create mode 100644 src/main/java/com/ceos24/springboot/TestRepository.java create mode 100644 src/main/java/com/ceos24/springboot/TestService.java diff --git a/src/main/java/com/ceos24/springboot/Test.java b/src/main/java/com/ceos24/springboot/Test.java new file mode 100644 index 0000000..ff10f4b --- /dev/null +++ b/src/main/java/com/ceos24/springboot/Test.java @@ -0,0 +1,14 @@ +package com.ceos24.springboot; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import lombok.Data; + +@Data +@Entity +public class Test { + + @Id + private Long id; + private String name; +} diff --git a/src/main/java/com/ceos24/springboot/TestController.java b/src/main/java/com/ceos24/springboot/TestController.java new file mode 100644 index 0000000..eef3e8a --- /dev/null +++ b/src/main/java/com/ceos24/springboot/TestController.java @@ -0,0 +1,20 @@ +package com.ceos24.springboot; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping(value = "/tests") +public class TestController { + + private final TestService testService; + + @GetMapping + public List findAllTests() { + return testService.findAllTests(); + } +} diff --git a/src/main/java/com/ceos24/springboot/TestRepository.java b/src/main/java/com/ceos24/springboot/TestRepository.java new file mode 100644 index 0000000..604c0e8 --- /dev/null +++ b/src/main/java/com/ceos24/springboot/TestRepository.java @@ -0,0 +1,6 @@ +package com.ceos24.springboot; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TestRepository extends JpaRepository { +} diff --git a/src/main/java/com/ceos24/springboot/TestService.java b/src/main/java/com/ceos24/springboot/TestService.java new file mode 100644 index 0000000..9925cd5 --- /dev/null +++ b/src/main/java/com/ceos24/springboot/TestService.java @@ -0,0 +1,18 @@ +package com.ceos24.springboot; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class TestService { + + private final TestRepository testRepository; + + @Transactional(readOnly = true) + public List findAllTests() { + return testRepository.findAll(); + } +} From 28643a34bfadf40f3543ff7ff500626ea45570f2 Mon Sep 17 00:00:00 2001 From: rinarina0429 Date: Wed, 9 Sep 2026 23:56:22 +0900 Subject: [PATCH 09/14] =?UTF-8?q?docs:=20README=20=EA=B3=BC=EC=A0=9C=20?= =?UTF-8?q?=EC=A0=9C=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 140 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/README.md b/README.md index 4162bb5..adaa2d9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,142 @@ # spring-tutorial-24th CEOS 백엔드 24기 스프링 튜토리얼 + +## 1️⃣ spring이 지원하는 기술들(IoC/DI, AOP, PSA 등)을 자유롭게 조사해요 + +### IoC/DI + +- 제어의 역전(Inversion of Control)과 의존성 주입(Dependency Injection) +- Spring에서는 IoC 컨테이너가 객체 간의 의존성을 주입 + → 코드가 인터페이스에만 의존하게 되어 결합도가 낮아짐 +- 공식 문서에서는 특히 생성자 주입(Constructor Injection)을 권장 + +```java +import org.springframework.stereotype.Service; + +@Service +public class UserService { + // 구체적인 구현체가 아닌 인터페이스에 의존 + private final UserRepository userRepository; + + // 스프링 컨테이너가 UserService를 생성할 때 UserRepository 구현체를 자동으로 주입 (DI) + public UserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public void registerUser() { + userRepository.save(); + } +} + +``` + +### AOP + +- Aspect-Oriented Programming +- 객체 지향 프로그래밍을 보완하는 구조 +- 애플리케이션 전반에 흩어져 있는 교차 관심사(ex. 로깅, 보안, 트랜잭션)를 별도의 모듈(Aspect)로 분리 +- 스프링은 런타임 프록시 방식으로 이를 구현 + +### PSA + +- 일관된 서비스 추상화 (Service Abstraction) +- Transaction, Cache, Messaging 등의 인프라 기술을 사용할 때, 특정 기술(JDBC, JPA, Hibernate 등)의 API에 종속되지 않고 동일한 프로그래밍 모델을 사용할 수 있도록 추상화를 제공 + +```java +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class OrderService { + + // JDBC, JPA, JTA 등 데이터 접근 기술이 무엇이든 상관없이 + // 동일한 어노테이션 하나로 트랜잭션 경계를 설정하고 롤백/커밋을 관리 + @Transactional + public void placeOrder(Order order) { + inventoryRepository.decreaseStock(order.getItemId()); + orderRepository.save(order); + } +} + +``` + +--- + +## 2️⃣ Spring Bean 이 무엇이고, Bean 의 라이프사이클과 Bean Scope에 대해 조사해요 + +### 1. Spring Bean, Lifecycle, Bean Scope + +- **Spring Bean 이란?**
+ 스프링 IoC 컨테이너에 의해 인스턴스화되고, 조립되며, 관리되는 객체 +- **Bean Lifecycle (생명주기)**
+ - 스프링 컨테이너는 빈의 생성부터 소멸까지의 과정을 관리하며, 특정 시점에 개발자가 개입할 수 있는 콜백(Callback)을 제공 + - **과정:** 인스턴스화 → 의존성 주입(DI) → 초기화 콜백(Initialization) → 빈 사용 → 소멸 콜백(Destruction) +- **Bean Scope (스코프)** + - 빈이 생성되고 존재하는 범위 + - **`singleton` (기본값):** 스프링 컨테이너당 단 하나의 공유 인스턴스만 생성되는 것 + - **`prototype`:** 컨테이너에 빈을 요청(주입)할 때마다 매번 새로운 인스턴스를 생성하는 것 + - **웹 전용 스코프:** 웹 환경에서만 유효하며 `request`(HTTP 요청당 하나), `session`(HTTP 세션당 하나), `application`(ServletContext당 하나), `websocket` 등 + +### 2. Java 어노테이션(Annotation)과 구현 + +- 어노테이션은 프로그램 소스 코드에 추가할 수 있는 메타데이터(Metadata) +- 프로그램의 비즈니스 로직 자체에는 직접적인 영향을 주지 않지만, 컴파일러에게 정보를 제공하거나 런타임 시 특정 기능을 수행하도록 프레임워크에 힌트를 준다. +- **Java에서의 구현:** + - `@interface` 키워드를 사용하여 선언 + - **메타 어노테이션:** `@Target`(적용 대상: 클래스, 메서드, 필드 등)과 `@Retention`(유지 정책: Source, Class, Runtime)을 통해 동작 방식을 정의 + - 스프링과 같은 프레임워크는 주로 `@Retention(RetentionPolicy.RUNTIME)`으로 설정된 어노테이션을 **리플렉션(Reflection) API**를 통해 런타임에 읽어들여 부가적인 처리를 수행 + +### 3. 스프링의 어노테이션 기반 Bean 등록 과정 + +스프링에서 `@Component`나 `@Bean` 어노테이션을 사용해 빈을 등록할 때 컨테이너 내부에서 일어나는 과정 + +1. **메타데이터 읽기:** 설정 클래스(예: `@Configuration`이 붙은 클래스) 읽기 +2. **BeanDefinition 생성:** 컨테이너는 즉시 객체를 생성하지 않고, 어노테이션 정보를 바탕으로 빈의 설계도격인 **`BeanDefinition`** 객체를 생성. 여기에는 빈의 클래스 이름, 스코프, 초기화 메서드, 의존성 정보 등이 담김. +3. **레지스트리 등록:** 생성된 `BeanDefinition`을 컨테이너 내부의 `BeanDefinitionRegistry`에 등록 +4. **인스턴스화 및 의존성 주입:** 빈 생성이 필요한 시점(싱글톤의 경우 컨테이너 초기화 시점)에 `BeanDefinition`을 기반으로 실제 Java 객체를 생성(Reflection 활용)하고, 필요한 의존성을 주입(DI) + +### 4. `@ComponentScan`의 컴포넌트 탐색 과정 심층 분석 + +`@ComponentScan`: 스프링이 어디서부터 컴포넌트를 찾을지(`basePackages`) 지정하고, 이를 자동으로 빈으로 등록하는 기능 + +1. **클래스패스 스캐닝 (Classpath Scanning):** `ClassPathBeanDefinitionScanner`가 지정된 베이스 패키지와 그 하위 패키지의 디렉토리를 파일 시스템이나 JAR 파일 내의 클래스패스에서 스캔 +2. **ASM을 통한 바이트코드 분석 (클래스 로딩 방지):** 스프링은 스캔 과정에서 메모리 낭비와 부작용을 막기 위해 런타임에 클래스를 직접 메모리에 로딩(Class Loading)하지 않고 대신 ASM이라는 바이트코드 조작 라이브러리를 기반으로 한 `MetadataReader`를 사용하여 `.class` 파일의 메타데이터만 빠르게 읽음 +3. **필터링 (Filtering):** 읽어들인 메타데이터 중 `@Component` 어노테이션(또는 이를 포함한 `@Service`, `@Repository`, `@Controller` 등의 메타 어노테이션)이 존재하는지 확인 +4. **BeanDefinition 등록:** 필터를 통과한 클래스들에 대해 `BeanDefinition`을 생성하고 레지스트리에 등록하여 이후 컨테이너가 빈으로 관리하게 함 + +--- + +## 3️⃣ 🔥Spring MVC를 심층 분석해요🔥 + +### 1. MVC 패턴 vs. Spring MVC + +- **MVC 패턴 (Model-View-Controller):**
+ 소프트웨어 공학에서 애플리케이션을 세 가지 역할로 나누는 **설계 아키텍처**로, 데이터(Model), 사용자 인터페이스(View), 그리고 이 둘을 연결하며 제어하는 비즈니스 로직(Controller)을 분리하여 유지보수성을 높이는 것이 목적 +- **Spring MVC (`spring-webmvc`):**
+ 스프링 프레임워크가 제공하는 웹 모듈로, MVC 아키텍처 패턴을 Java의 Servlet API를 기반으로 구현한 **실제 프레임워크**. 일반적인 MVC 구조에 중앙 집중식 요청 처리기인 **프론트 컨트롤러(Front Controller)** 패턴을 도입하여 웹 요청 처리를 표준화하고 유연한 확장을 제공. + +### 2. Servlet과 웹 요청 처리 + +* **Servlet (서블릿):** Java를 사용하여 웹 서버의 기능을 확장하고 동적인 HTTP 응답을 생성하기 위한 자바 표준 인터페이스(규약)
+ (Spring MVC 자체가 이 Servlet API를 기반으로 구축되어 있음) +* **웹 요청 처리 흐름:** + 1. 클라이언트(브라우저)가 HTTP 요청을 서버로 전송 + 2. 서블릿 컨테이너가 요청을 받아 `HttpServletRequest`와 `HttpServletResponse` 객체를 생성 + 3. 요청 URL에 매핑된 서블릿의 `service()` 메서드를 호출. (HTTP 메서드에 따라 내부적으로 `doGet`, `doPost` 등으로 분기) + 4. 로직 처리가 끝나면 `HttpServletResponse`를 통해 클라이언트에게 응답을 보내고, 생성되었던 Request/Response 객체를 소멸시킴 + +### 3. 톰캣(Tomcat)과 WAS + +* **WAS (Web Application Server):** 정적 콘텐츠(HTML, 이미지)만 제공하는 웹 서버와 달리, DB 조회나 복잡한 비즈니스 로직을 통해 **동적인 콘텐츠**를 생성하고 제공하는 미들웨어. 내부적으로 서블릿을 실행할 수 있는 '서블릿 컨테이너'를 포함. +* **Tomcat:** 아파치 소프트웨어 재단에서 개발한 가장 대표적인 오픈소스 서블릿 컨테이너이자 WAS. 스프링 부트(Spring Boot)는 이 톰캣을 애플리케이션 내부에 내장(Embedded Tomcat)하여 제공하므로, 개발자가 별도의 서버 설정 없이 `main()` 메서드 실행만으로 웹 서버를 띄울 수 있음. + +### 4. DispatcherServlet과 웹 요청 동작 흐름 + +스프링 공식 문서는 Spring MVC의 핵심을 `DispatcherServlet`으로 정의함. 이는 모든 HTTP 요청을 가장 먼저 받아 적절한 컴포넌트로 요청을 위임하는 **프론트 컨트롤러(Front Controller)** 역할을 수행. + +**동작 흐름:** + +1. 요청 수신 +2. 핸들러 조회 (`HandlerMapping`) +3. 어댑터 조회 및 실행 (`HandlerAdapter`) +4. 결과 처리 및 응답 반환 \ No newline at end of file From e380c62f033770f02c80605926d2b657848d1215 Mon Sep 17 00:00:00 2001 From: rinarina0429 Date: Thu, 10 Sep 2026 01:00:44 +0900 Subject: [PATCH 10/14] =?UTF-8?q?docs:=20IoC/DI=20=EB=82=B4=EC=9A=A9=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 76 ++++++++++++++++++++++++++++++++++++---- img/spring_triangle.png | Bin 0 -> 14760 bytes 2 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 img/spring_triangle.png diff --git a/README.md b/README.md index adaa2d9..362801f 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,72 @@ CEOS 백엔드 24기 스프링 튜토리얼 ## 1️⃣ spring이 지원하는 기술들(IoC/DI, AOP, PSA 등)을 자유롭게 조사해요 -### IoC/DI +스프링에는 세가지 핵심 기술이 있습니다. 스프링 삼각형이라고도 부르는 3대 핵심 요소는 IoC/DI, AOP, PSA로, 이들은 서로 매우 긴밀하게 협력하여 동작합니다. 이들의 공통된 최종 목표는 순수한 자바 객체(POJO)를 유지하면서 결합도를 낮추고 유연한 서버 애플리케이션을 만드는 것입니다. -- 제어의 역전(Inversion of Control)과 의존성 주입(Dependency Injection) -- Spring에서는 IoC 컨테이너가 객체 간의 의존성을 주입 - → 코드가 인터페이스에만 의존하게 되어 결합도가 낮아짐 -- 공식 문서에서는 특히 생성자 주입(Constructor Injection)을 권장 + + +근데? 사실 백번 읽어봐도 모르겠으니! 하나씩 공부해봅시다. + +### ① IoC/DI + +IoC/DI는 제어의 역전(Inversion of Control)과 의존성 주입(Dependency Injection)의 줄임말입니다. +Spring에서는 **IoC 컨테이너**가 객체 간의 의존성을 주입합니다. +여기서 '의존성 주입'은 '제어의 역전'의 특수한 형태입니다. +

+예시를 통해 조금 더 쉽게 살펴보도록 하겠습니다. +
+요리사가 국수를 요리하는 상황을 자바 코드로 비교해봅시다. + +```java +public class Chef { +private PorkSpine meat; + + public Chef() { + // 요리사가 직접 구체적인 객체를 생성합니다 (제어권이 요리사에게 있음) + this.meat = new PorkSpine(); + } + + public void cook() { + System.out.println("푹 고아낸 " + meat.getName() + " 국수 완성!"); + } +} +``` +위 코드에서, 요리사는 직접 구체적인 객체를 생성합니다. 즉, 제어권이 요리사에게 있죠. 하지만 이 경우 돼지등뼈가 소진되면 어떻게 해야 할까요? 요리사가 직접 정육점까지 뛰어가 돼지등뼈를 사와야 하는 상황이 발생합니다. +
+하지만 요리사가 아닌, 사장님이 출근길에 고기를 사오면 훨씬 좋지 않을까요? 그리고 돼지등뼈가 없어도, 다른 고기를 넣을 수 있으면 더 편하지 않을까요? + +```java +@Component +public class Chef { + private final Meat meat; + + // 요리사는 추상적인 '고기(Meat 인터페이스)'면 뭐든 받아서 요리합니다. + // 객체 생성과 공급의 제어권(IoC)이 스프링 사장님에게 넘어갔습니다! + @Autowired + public Chef(Meat meat) { + this.meat = meat; + } + + public void cook() { + System.out.println("푹 고아낸 " + meat.getName() + " 국수 완성!"); + } +} +``` +식당 사장님, 즉 스프링 컨테이너에게 제어권을 넘기면 요리사는 "고기"이기만 하면 뭐든 받아 요리하면 되는 편리한 상황이 됩니다. +또한 코드가 인터페이스에만 의존하게 되어 결합도가 낮아지죠. +
+위 예시 코드에서 본 방식은 생성자 주입(Constructor Injection) 방식입니다. +
+사실 의존성을 주입하는 방법에는 생성자 주입 뿐만 아니라 수정자 주입(Setter Injection), 필드 주입(Field Injection) 방식도 있습니다. 하지만 공식 문서에서는 특히 생성자 주입 방식을 권장합니다. +- 우선 생성 시점에 딱 한 번만 호출되므로 주입받은 의존성이 런타임에 변하지 않음(**불변성**)을 보장합니다. +- 또한 필드에 final 키워드를 사용할 수 있어, 만약 의존성 주입을 깜빡하더라도 애플리케이션 실행 전 컴파일 단계에서 바로 에러를 잡아낼 수 있습니다. 즉, **필수 의존성**을 보장하는거죠. +- 그 외에도 순환 참조를 방지할 수 있고, 테스트 시에 Mock 객체를 사용하기에 용이하다는 등의 이점이 있습니다. + +그럼 이제 제어가 역전된다는게 어떤 느낌인지 알겠죠? +
+만약 아직도 이해가 안된다면 [의존성 주입 3분만에 이해하기 (Dependency Injection, Inversion of Control)](https://youtu.be/1vdeIL2iCcM?si=_hvHcxvKs-Z8tq3b) 영상을 추천드립니다. +
+이해가 되셨다면! 실제 스프링에서는 아래와 같이 제어의 역전이 사용됩니다. ```java import org.springframework.stereotype.Service; @@ -139,4 +199,8 @@ public class OrderService { 1. 요청 수신 2. 핸들러 조회 (`HandlerMapping`) 3. 어댑터 조회 및 실행 (`HandlerAdapter`) -4. 결과 처리 및 응답 반환 \ No newline at end of file +4. 결과 처리 및 응답 반환 + +--- + +이번 과제를 수행하면서 관련된 개념들에 대한 유튜브 영상들로부터 많은 도움을 받았습니다. 제가 집중력이 짧아 영상의 도움을 받은 것도 있지만... 생각보다 유튜브에 고수들이 너무나 쉽게 풀어서 설명한 영상들이 참 많답니다? 모두 알고 계시겠지만 한번 상기시켜드리고 갑니다...👍 \ No newline at end of file diff --git a/img/spring_triangle.png b/img/spring_triangle.png new file mode 100644 index 0000000000000000000000000000000000000000..50642239ad2e14d8d1910048e2c99cfc28b16556 GIT binary patch literal 14760 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 Date: Thu, 10 Sep 2026 01:58:05 +0900 Subject: [PATCH 11/14] =?UTF-8?q?docs:=20AOP=20=EB=82=B4=EC=9A=A9=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 57 +++++++++++++++++++++++++++++++++++++++++---- img/spring_aop.png | Bin 0 -> 43182 bytes 2 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 img/spring_aop.png diff --git a/README.md b/README.md index 362801f..c6392bb 100644 --- a/README.md +++ b/README.md @@ -90,12 +90,59 @@ public class UserService { ``` -### AOP +### ② AOP -- Aspect-Oriented Programming -- 객체 지향 프로그래밍을 보완하는 구조 -- 애플리케이션 전반에 흩어져 있는 교차 관심사(ex. 로깅, 보안, 트랜잭션)를 별도의 모듈(Aspect)로 분리 -- 스프링은 런타임 프록시 방식으로 이를 구현 +자, 이제 AOP입니다. 여러분은 모두 AOP를 잘 사용하고 있을겁니다. 자주 쓰이는 `@Transactional` 또한 AOP의 일종이라고 할 수 있기 때문이죠. +이런 AOP는 Aspect-Oriented Programming의 줄임말데요, 말 그대로 관점 지향 프로그래밍을 뜻합니다. +AOP, 관점 지향 프로그래밍이라는 말만 들으면 OOP, 객체 지향 프로그래밍과 공존할 수 없는 것처럼 들릴 수 있습니다. 하지만 AOP는 OOP 프로그래밍을 보완하는 구조입니다. +실제로 [Spring의 공식 문서](https://docs.spring.io/spring-framework/reference/core/aop.html#page-title)에는 다음과 같이 쓰여있습니다. + +> Aspect-oriented Programming (AOP) complements Object-oriented Programming (OOP) by providing another way of thinking about program structure. + +그리고 관점 지향은 쉽게 말해, '핵심 비즈니스 로직'과 '반복되는 부가 기능'을 완벽하게 분리해 내는 기술입니다. 즉, 애플리케이션 전반에 흩어져 있는 교차 관심사(ex. 로깅, 보안, 트랜잭션)를 별도의 모듈(Aspect)로 분리합니다. +

+예를 들어 은행 시스템을 개발한다고 상상해 볼까요? +

+ +

+'계좌 이체', '대출 승인', '잔액 조회'처럼 각 모듈이 수행해야 하는 진짜 비즈니스 로직이 있습니다. 반면, '보안 검사', 'DB 연동(트랜잭션)', '실행 시간 로깅' 같은 인프라 로직은 이체, 대출, 조회 기능을 실행할 때마다 공통으로 앞뒤에 들어가야 하죠. +
+이런 여러 고유 기능들(세로)을 가로지르며(횡단하며) 똑같이 나타나는 로직들을 '횡단 관심사(Cross-cutting Concerns)'라고 부릅니다. +AOP는 이렇게 가로로 겹치는 횡단 관심사들을 가위로 오려내어 별도의 클래스(Aspect)에 따로 모아둡니다. 그리고 프레임워크에 이 코드들을 런타임 시 앞뒤로 끼워 넣어달라고 설정해둡니다. +결과적으로 개발자는 복잡한 인프라 코드를 신경 쓸 필요 없이, '계좌 이체'라는 순수한 핵심 로직 작성에만 100% 집중할 수 있게 되는거죠. +
+만약 AOP가 없다면 개발자는 계좌 이체 코드에도, 대출 승인 코드에도 매번 보안검사(), 트랜잭션시작(), 로깅() 코드를 똑같이 복사해서 붙여넣어야 할겁니다. 보안 검사 방식이 업데이트라도 되면 수백 개의 파일을 열어 일일이 수정해야 하는 끔찍한 일이 벌어지고, 비즈니스 로직은 단 3줄인데, 부가 기능 코드가 20줄을 차지해 코드를 읽기도 힘들어지게 됩니다. +

+그러면 AOP는 어떻게 적용할 수 있을까요? AOP를 실제 코드에 적용하는 방식은 시점에 따라 크게 컴파일 타임, 클래스 로드 타임, 런타임 세 가지로 나뉩니다. 그리고 Spring AOP는 이 중 프록시 패턴을 기반으로 한 런타임(Run-time) 방식을 사용합니다. +스프링 컨테이너가 IoC/DI를 통해 객체(Bean)의 생성과 의존성 주입을 직접 통제하기 때문에, 실제 객체 대신 부가 기능이 씌워진 프록시 객체를 런타임에 동적으로 주입할 수 있는거죠. +
+아래와 같은 예시 코드가 AOP를 잘 보여줍니다. + +```java +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; + +@Aspect +@Component +public class LoggingAspect { + + // Service 클래스의 모든 메서드 실행 전후에 공통 로깅 기능 적용 + @Around("execution(* com.example..*Service.*(..))") + public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { + long start = System.currentTimeMillis(); + + Object proceed = joinPoint.proceed(); // 실제 핵심 비즈니스 로직 실행 + + long executionTime = System.currentTimeMillis() - start; + System.out.println(joinPoint.getSignature() + " 실행 시간: " + executionTime + "ms"); + return proceed; + } +} +``` + +더 나은 AOP의 이해를 위해 [[10분 테코톡] 🌕제이의 Spring AOP](https://youtu.be/Hm0w_9ngDpM?si=hRbn6y1DWXYCic4F) 영상을 추천드립니다. ### PSA diff --git a/img/spring_aop.png b/img/spring_aop.png new file mode 100644 index 0000000000000000000000000000000000000000..9686395638fb5604651f4964aab274329ac7af7e GIT binary patch literal 43182 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 M$?wa)JOAVV1DVXQ6#xJL literal 0 HcmV?d00001 From 5b785bc67037ebc0721929b7f5aa23ed78398069 Mon Sep 17 00:00:00 2001 From: rinarina0429 Date: Thu, 10 Sep 2026 12:44:47 +0900 Subject: [PATCH 12/14] =?UTF-8?q?docs:=20PSA=20=EB=82=B4=EC=9A=A9=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 65 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index c6392bb..4b16858 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ CEOS 백엔드 24기 스프링 튜토리얼 스프링에는 세가지 핵심 기술이 있습니다. 스프링 삼각형이라고도 부르는 3대 핵심 요소는 IoC/DI, AOP, PSA로, 이들은 서로 매우 긴밀하게 협력하여 동작합니다. 이들의 공통된 최종 목표는 순수한 자바 객체(POJO)를 유지하면서 결합도를 낮추고 유연한 서버 애플리케이션을 만드는 것입니다. - + 근데? 사실 백번 읽어봐도 모르겠으니! 하나씩 공부해봅시다. @@ -144,29 +144,68 @@ public class LoggingAspect { 더 나은 AOP의 이해를 위해 [[10분 테코톡] 🌕제이의 Spring AOP](https://youtu.be/Hm0w_9ngDpM?si=hRbn6y1DWXYCic4F) 영상을 추천드립니다. -### PSA +### ③ PSA -- 일관된 서비스 추상화 (Service Abstraction) -- Transaction, Cache, Messaging 등의 인프라 기술을 사용할 때, 특정 기술(JDBC, JPA, Hibernate 등)의 API에 종속되지 않고 동일한 프로그래밍 모델을 사용할 수 있도록 추상화를 제공 +마지막으로 PSA입니다. PSA는 Portable Service Abstraction의 줄임말로, 일관된 서비스 추상화라고도 합니다. -```java -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; +쉽게 말하면 PSA는 어떤 기술을 사용하든 개발자가 비슷한 방식으로 사용할 수 있도록 중간에 추상화 계층을 만들어주는 것입니다. + +예를 들어 우리가 해외여행을 간다고 생각해봅시다. +
+한국에서는 콘센트에 충전기를 바로 꽂을 수 있지만, 다른 나라에서는 콘센트 모양이나 전압이 다를 수 있습니다. 그렇다고 여행을 갈 때마다 노트북이나 휴대폰의 충전 방식을 전부 고칠 수는 없겠죠. +그래서 우리는 어댑터를 사용합니다. 사용자는 똑같은 충전기를 사용하고, 어댑터가 각 나라의 콘센트 규격에 맞게 변환해주죠. + +Spring의 PSA도 비슷합니다. +개발자가 특정 기술의 세부적인 사용 방법을 일일이 알지 못하더라도, Spring이 제공하는 일관된 인터페이스를 사용하면 내부 구현 기술을 비교적 자유롭게 변경할 수 있도록 만들어줍니다. +대표적인 예가 바로 트랜잭션 추상화입니다. +앞의 AOP 설명에서 나왔던 `@Transactional`을 다시 생각해볼까요? + +```java @Service public class OrderService { - - // JDBC, JPA, JTA 등 데이터 접근 기술이 무엇이든 상관없이 - // 동일한 어노테이션 하나로 트랜잭션 경계를 설정하고 롤백/커밋을 관리 + @Transactional - public void placeOrder(Order order) { - inventoryRepository.decreaseStock(order.getItemId()); - orderRepository.save(order); + public void createOrder() { + // 주문 저장 + // 결제 처리 + // 재고 감소 } } +``` +개발자인 우리는 그냥 `@Transactional`을 붙입니다. +
+그런데 실제로 트랜잭션을 처리하는 기술은 프로젝트마다 다를 수 있습니다. +어떤 프로젝트에서는 JDBC를 사용할 수도 있고, 다른 프로젝트에서는 JPA를 사용할 수도 있고, 또 다른 환경에서는 JTA와 같은 트랜잭션 기술을 사용할 수도 있습니다. +만약 Spring의 추상화가 존재하지 않는다면 사용하는 기술이 바뀔 때마다 개발자가 트랜잭션 시작, 커밋, 롤백을 처리하는 코드를 해당 기술의 API에 맞게 다시 작성해야 할 수도 있습니다. +하지만 Spring은 이들 사이에 트랜잭션 추상화 계층을 제공합니다. +그 중심에 있는 인터페이스가 `PlatformTransactionManager`입니다. + +```java +public interface PlatformTransactionManager extends TransactionManager { + + TransactionStatus getTransaction(TransactionDefinition definition) + throws TransactionException; + + void commit(TransactionStatus status) + throws TransactionException; + + void rollback(TransactionStatus status) + throws TransactionException; +} ``` +실제로 사용하는 기술에 따라서 이 인터페이스의 구현체가 달라집니다. +예를 들어 JPA를 사용한다면 `JpaTransactionManager`가 사용될 수 있고, JDBC 기반 환경이라면 JDBC에 맞는 TransactionManager가 사용될 수 있습니다. +중요한 것은 비즈니스 로직을 작성하는 개발자가 이러한 차이를 직접 처리하지 않아도 된다는거죠. +Spring의 공식 문서에서도 Spring의 트랜잭션 지원이 JDBC, JTA, Hibernate, JPA처럼 서로 다른 트랜잭션 API에 대해 일관된 프로그래밍 모델을 제공한다고 설명합니다. + +즉, 우리의 비즈니스 코드는 그대로 두고, 내부에서 사용하는 트랜잭션 기술이나 구현체를 변경할 수 있게 되는 것입니다. +덕분에 애플리케이션의 상위 계층이 특정 DB 기술의 예외 클래스에 강하게 의존하는 것을 줄일 수 있습니다. + +결국 한줄로 정리하면, PSA의 핵심은 "구체적인 기술은 바뀌어도, 그것을 사용하는 애플리케이션의 코드는 최대한 바뀌지 않도록 만드는 것"이 아닐까요? + --- ## 2️⃣ Spring Bean 이 무엇이고, Bean 의 라이프사이클과 Bean Scope에 대해 조사해요 From 834e2447966110a6d1450164bd0385ae47d991f4 Mon Sep 17 00:00:00 2001 From: rinarina0429 Date: Thu, 10 Sep 2026 13:02:19 +0900 Subject: [PATCH 13/14] =?UTF-8?q?docs:=20Spring=20=EA=B8=B0=EC=88=A0=20?= =?UTF-8?q?=EB=82=B4=EC=9A=A9=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/README.md b/README.md index 4b16858..1f99d7a 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,78 @@ Spring의 공식 문서에서도 Spring의 트랜잭션 지원이 JDBC, JTA, Hib 결국 한줄로 정리하면, PSA의 핵심은 "구체적인 기술은 바뀌어도, 그것을 사용하는 애플리케이션의 코드는 최대한 바뀌지 않도록 만드는 것"이 아닐까요? +### ✔ 그래서, IoC/DI, AOP, PSA는 서로 무슨 관계인데? + +지금까지 세 가지를 따로 공부했지만, 사실 이 기술들은 서로 독립적으로 존재하는 것이 아닙니다. +
+처음에 Spring의 세 가지 핵심 기술이 서로 매우 긴밀하게 협력한다고 했던 것을 기억하시나요? +
+`@Transactional` 하나만 살펴봐도 세 기술이 어떻게 협력하는지 알 수 있습니다. + +```java +@Service +public class OrderService { + + private final OrderRepository orderRepository; + + public OrderService(OrderRepository orderRepository) { + this.orderRepository = orderRepository; + } + + @Transactional + public void createOrder() { + orderRepository.save(); + } +} +``` + +먼저 **IoC/DI**를 통해 Spring 컨테이너가 `OrderService`, `OrderRepository`, `TransactionManager` 등의 객체를 생성하고 서로 연결해줍니다. +그리고 `@Transactional`이 붙은 메서드에는 **AOP**가 적용됩니다. +Spring은 실제 `OrderService` 앞에 프록시 객체를 두고 메서드 실행 전 트랜잭션을 시작하고, 정상적으로 끝나면 커밋하고, 문제가 생기면 롤백합니다. +Spring의 선언적 트랜잭션 역시 이러한 프록시와 인터셉터 구조를 사용합니다. +마지막으로 실제 트랜잭션 처리는 PSA를 통해 추상화되어 있습니다. +개발자는 JDBC인지 JPA인지 JTA인지에 따라 서로 다른 코드를 작성하는 대신 Spring의 일관된 트랜잭션 추상화를 사용합니다. + +결국 세 가지를 합쳐보면 다음과 같습니다. + +```bash + Spring Container + │ + IoC / DI + │ + ▼ + ┌────────────────┐ + │ OrderService │ + │ │ + │ createOrder() │← 핵심 비즈니스 로직 + └────────────────┘ + ▲ + │ + AOP Proxy + ┌────────┴────────┐ + │ Transaction │ + │ Logging │ + │ Security ... │ + └─────────────────┘ + │ + PSA + ┌────────────┼────────────┐ + ▼ ▼ ▼ + JDBC JPA JTA +``` + +- IoC/DI는 객체 간의 관계를 유연하게 만들고, +- AOP는 반복되는 부가 기능을 핵심 로직으로부터 분리하며, +- PSA는 구체적인 기술의 차이를 추상화합니다. + +그리고 그 결과 개발자는 특정 프레임워크나 인프라 기술에 지나치게 얽매이지 않은 POJO 중심의 비즈니스 로직을 작성할 수 있게 됩니다. + +이제 Spring 코드를 보면 + +> "IoC/DI로 관리되는 Bean에 AOP 프록시가 적용되고, 그 뒤에서는 Spring의 서비스 추상화를 통해 실제 기술이 동작하고 있구나!" + +라고 생각할 수 있지 않을까요? 😽 + --- ## 2️⃣ Spring Bean 이 무엇이고, Bean 의 라이프사이클과 Bean Scope에 대해 조사해요 From fbbb000fd93adc9b560121bcc1b27461df3eef19 Mon Sep 17 00:00:00 2001 From: rinarina0429 Date: Fri, 11 Sep 2026 13:31:59 +0900 Subject: [PATCH 14/14] =?UTF-8?q?docs:=20Spring=20Bean=20=EB=82=B4?= =?UTF-8?q?=EC=9A=A9=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1085 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 1054 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 1f99d7a..5bfa1b4 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ Spring의 공식 문서에서도 Spring의 트랜잭션 지원이 JDBC, JTA, Hib 즉, 우리의 비즈니스 코드는 그대로 두고, 내부에서 사용하는 트랜잭션 기술이나 구현체를 변경할 수 있게 되는 것입니다. 덕분에 애플리케이션의 상위 계층이 특정 DB 기술의 예외 클래스에 강하게 의존하는 것을 줄일 수 있습니다. -결국 한줄로 정리하면, PSA의 핵심은 "구체적인 기술은 바뀌어도, 그것을 사용하는 애플리케이션의 코드는 최대한 바뀌지 않도록 만드는 것"이 아닐까요? +결국 한줄로 정리하면, PSA의 핵심은 "구체적인 기술은 바뀌어도, 그것을 사용하는 애플리케이션의 코드는 최대한 바뀌지 않도록 만드는 것"이라고 할 수 있을 것 같습니다. ### ✔ 그래서, IoC/DI, AOP, PSA는 서로 무슨 관계인데? @@ -282,45 +282,1068 @@ Spring의 선언적 트랜잭션 역시 이러한 프록시와 인터셉터 구 ## 2️⃣ Spring Bean 이 무엇이고, Bean 의 라이프사이클과 Bean Scope에 대해 조사해요 -### 1. Spring Bean, Lifecycle, Bean Scope +Spring을 사용하다 보면 Bean이라는 단어를 자주 듣게 됩니다. -- **Spring Bean 이란?**
- 스프링 IoC 컨테이너에 의해 인스턴스화되고, 조립되며, 관리되는 객체 -- **Bean Lifecycle (생명주기)**
- - 스프링 컨테이너는 빈의 생성부터 소멸까지의 과정을 관리하며, 특정 시점에 개발자가 개입할 수 있는 콜백(Callback)을 제공 - - **과정:** 인스턴스화 → 의존성 주입(DI) → 초기화 콜백(Initialization) → 빈 사용 → 소멸 콜백(Destruction) -- **Bean Scope (스코프)** - - 빈이 생성되고 존재하는 범위 - - **`singleton` (기본값):** 스프링 컨테이너당 단 하나의 공유 인스턴스만 생성되는 것 - - **`prototype`:** 컨테이너에 빈을 요청(주입)할 때마다 매번 새로운 인스턴스를 생성하는 것 - - **웹 전용 스코프:** 웹 환경에서만 유효하며 `request`(HTTP 요청당 하나), `session`(HTTP 세션당 하나), `application`(ServletContext당 하나), `websocket` 등 +```java +@Service +public class UserService { + ... +} +``` +이런 코드를 작성할 때도, "`UserService`는 Spring Bean으로 등록된다"라고 하는데요, +우린 그저 평범한 Java 클래스에 `@Service`라는 어노테이션을 붙인 것 뿐인데 대체 Spring은 이 클래스를 어떻게 발견하는거고, +Bean으로 등록된다는건 정확히 무슨 뜻인걸까요? + +Spring Bean이 애플리케이션 시작 과정에서 어떻게 발견되고, 만들어지고, 사용되다가, 사라지는지 하나씩 알아봅시다! + +### Spring Bean? + +먼저 Bean부터 알아봅시다. +Spring Bean이란, Spring IoC Container가 생성하고 관리하는 객체입니다. + +> A bean is an object that is instantiated, assembled, and managed by a Spring IoC container. +>
+> [Spring 공식문서](https://docs.spring.io/spring-framework/reference/core/beans/introduction.html) + +예를 들어 아래와 같은 평범한 객체가 있다고 해볼까요? + +```java +public class Chef { + + public void cook() { + System.out.println("국수 완성!"); + } +} +``` + +우리가 직접 객체를 생성하면, + +```java +Chef chef = new Chef(); +``` + +이 `Chef` 객체의 생성과 관리에 대한 책임은 전적으로 개발자에게 있습니다. + +반면, + +```java +@Component +public class Chef { + + public void cook() { + System.out.println("국수 완성!"); + } +} +``` + +Spring이 `Chef`를 발견하고 객체를 생성해서 Spring Container 안에서 관리하기 시작하면, 이 객체를 **Spring Bean**이라고 부릅니다. + +Spring 공식 문서에서도 Bean을 구성하기 위한 정보를 **Bean Definition**으로 관리하며, 실제 애플리케이션 객체들은 이러한 Bean Definition을 바탕으로 Spring Container가 생성하고 관리한다고 설명합니다. + +그런데 여기서 중요한 개념 하나가 등장합니다. + +바로 **BeanDefinition**입니다. +
+BeanDefinition은 쉽게 말하면 Bean을 만들기 위한 설계도라고 볼 수 있습니다. +예를 들어 Spring은, 먼저 + +_"UserService라는 클래스를 +singleton으로 만들고, +이런 의존성을 주입해서, +userService라는 이름으로 관리해야겠다."_ + +라는 정보를 등록합니다. +
+이 정보가 바로 `BeanDefinition`입니다. + +그리고 이후 실제 Bean을 생성할 시점이 되면 Spring Container가 이 설계도를 보고 객체를 생성합니다. + +```text +@Component 발견 + ↓ +BeanDefinition 생성 + ↓ +BeanDefinitionRegistry에 등록 + ↓ +필요한 시점에 실제 객체 생성 + ↓ +Spring Bean +``` + +### Annotation + +그런데, 어노테이션은 대체 뭘까요? +
+Spring을 사용하면 정말 많은 어노테이션을 사용합니다. + +`@Component`, `@Service`, `@Repository`, `@Controller`, `@Autowired`, `@Transactional`, `@Configuration`, `@Bean`, ... + +Java에서 Annotation은 "코드에 추가적인 정보를 붙여두는 메타데이터"입니다. + +예를 들어 우리가 직접 어노테이션을 만들 수도 있습니다. +```java +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface MyAnnotation { + + String value() default ""; +} +``` + +그리고 아래와 같이 사용할 수 있습니다. + +```java +@MyAnnotation("hello") +public class UserService { +} +``` + +하지만 여기서 어노테이션 자체는 아무런 행동도 하지 않는데요. +`@MyAnnotation("hello")`를 붙였다고 갑자기 어떤 코드가 실행되는 것이 아니라, 누군가 이 어노테이션을 읽고, 의미를 부여해야 하는거죠. + +Java의 클래스는 `getAnnotation()`, `isAnnotationPresent()` 같은 API를 제공하므로 런타임에 특정 어노테이션이 존재하는지 확인할 수 있습니다. + +그럼 우리도 아래와 같이 어노테이션을 직접 읽어볼 수도 있겠죠? + +```java +MyAnnotation annotation = + UserService.class.getAnnotation(MyAnnotation.class); + +System.out.println(annotation.value()); +``` + +결과는 다음과 같습니다. + +```text +hello +``` + +즉, Annotation은 메타데이터이고, Annotation에 그것을 해석하는 프로그램이 함께하면 실제 기능이 된다고 이해할 수 있을 것 같습니다. + +Spring도 마찬가지인데요, UserService에 붙은 `@Service`가 단순히 스스로 객체를 만드는 것이 아닙니다. +Spring이 `@Service`라는 메타데이터를 발견하고, 이 클래스는 내가 관리해야겠다고 판단하기 때문에 Bean으로 등록되는 것입니다. + +그럼 Java에서 어노테이션은 어떻게 구현하는지 조금 더 살펴볼까요? + +Java의 어노테이션은 `@interface`라는 문법으로 정의합니다. + +```java +public @interface MyAnnotation {} +``` + +이 문법은 단순히 `interface` 앞에 골뱅이를 붙인 이상한 문법처럼 보이지만, Java Language Specification에서는 Annotation Interface를 일반 Interface와 구분되는 특별한 형태의 Interface로 정의하고 있습니다. +또한 Annotation Interface는 직접적으로 `java.lang.annotation.Annotation`을 상위 인터페이스로 가집니다. + +어노테이션을 만들 때 자주 등장하는 것이 바로 메타 어노테이션(Meta Annotation)인데요, +
+대표적으로 다음과 같은 것들이 있습니다. + +| 어노테이션 | 역할 | +| ------------- | ------------------------------------ | +| `@Target` | 이 어노테이션을 어디에 붙일 수 있는지 지정 | +| `@Retention` | 어노테이션 정보를 언제까지 유지할지 지정 | +| `@Documented` | JavaDoc 등에 어노테이션 정보를 포함 | +| `@Inherited` | 자식 클래스가 부모 클래스의 어노테이션을 상속받을 수 있도록 설정 | + +예를 들어, + +```java +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface MyAnnotation { +} +``` + +이라고 정의했다면, + +`@Target(ElementType.TYPE)`은 클래스나 인터페이스 같은 **타입에 붙일 수 있다**는 의미이고, +
+`@Retention(RetentionPolicy.RUNTIME)`은 **실행 중에도 해당 어노테이션 정보를 확인할 수 있도록 유지한다**는 뜻입니다. + +Retention에는 크게 세 가지 정책이 있습니다. + +- `SOURCE` + - 소스 코드에만 존재 + - 컴파일 후 사라짐 +- `CLASS` + - .class 파일까지 존재 + - 일반적으로 Reflection을 통해 런타임에 사용할 수 없음 +- `RUNTIME` + - .class 파일에도 존재 + - 런타임에도 조회 가능 + +`@Retention`을 지정하지 않으면 기본값은 `CLASS`이며, `RUNTIME`으로 지정한 어노테이션은 Java Reflection API에서도 사용할 수 있습니다. +
+Spring의 `@Component` 역시 `RUNTIME`으로 유지되는 어노테이션입니다. + +그럼 자주 쓰이는 `@Component`, `@Service`, `@Repository`, `@Controller`는 뭐가 다른걸까요? +
+사실 이들의 뿌리는 모두 `@Component`입니다. +
+예를 들어, 개념적으로 `@Service`는 다음과 같은 구조를 가지고 있습니다. + +```java +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Component +public @interface Service { + + String value() default ""; +} +``` + +즉, `@Service` 위에 다시 `@Component`가 붙어 있습니다. +
+이렇게 **어노테이션 위에 붙어 있는 어노테이션**을 Meta Annotation이라고 합니다. + +따라서 Spring은 + +```java +@Component +public class A { +} +``` + +뿐만 아니라 + +```java +@Service +public class B { +} +``` + +도 `@Component` 계열의 Bean 후보로 인식할 수 있습니다. + +Spring 공식 문서에서도 `@Service`, `@Repository`, `@Controller`를 `@Component`의 특수화된 stereotype이라고 설명합니다. +또한 `@Component`를 Meta Annotation으로 가지고 있는 사용자 정의 어노테이션 역시 컴포넌트 스캔의 대상이 될 수 있습니다. + +### @ComponentScan + +그런데 Spring은 `@Service`가 붙은 클래스를 어떻게 찾는 걸까요? + +우리는 Spring Boot 애플리케이션을 만들면 보통 다음 코드로 시작합니다. + +```java +@SpringBootApplication +public class MyApplication { + + public static void main(String[] args) { + SpringApplication.run(MyApplication.class, args); + } +} +``` + +그런데 어디에도 + +`new UserService();`, `new OrderService();`, `new PaymentService();` 같은 코드는 없습니다. + +그런데도 Spring은 우리가 만든 수많은 Service, Repository, Controller를 알아서 찾아냅니다. +그 방법 중 하나가 바로 `@ComponentScan`입니다. + +`@SpringBootApplication`을 뜯어볼까요? +
+`@SpringBootApplication`은 하나의 단순한 어노테이션처럼 보이지만 내부적으로 여러 설정을 합쳐놓은 합성 어노테이션(Composed Annotation)입니다. + +핵심적으로 아래와 같은 세 기능을 포함합니다. + +```text +@SpringBootApplication + │ + ├── @SpringBootConfiguration + ├── @EnableAutoConfiguration + └── @ComponentScan +``` + +즉 우리가 직접 `@ComponentScan`을 작성하지 않아도 Spring Boot의 `@SpringBootApplication`이 Component Scan 기능을 활성화 하는거죠. + +그럼 `@ComponentScan`은 어디까지 탐색할까요? + +다음과 같은 프로젝트가 있다고 해봅시다. + +```text +com.example.myapp +│ +├── MyApplication.java +│ +├── controller +│ └── UserController.java +│ +├── service +│ └── UserService.java +│ +└── repository + └── UserRepository.java +``` + +`MyApplication`이 + +```java +package com.example.myapp; + +@SpringBootApplication +public class MyApplication { +} +``` + +에 있다면 기본적인 컴포넌트 스캔 범위는 `com.example.myapp`와 그 하위 패키지가 됩니다. +
+따라서 `com.example.myapp.controller`, `com.example.myapp.service`, `com.example.myapp.repository`에 있는 컴포넌트들을 발견할 수 있습니다. + +Spring의 `@ComponentScan`은 별도의 `basePackages`가 지정되지 않으면 **해당 어노테이션을 선언한 클래스의 패키지부터 탐색**합니다. +Spring Boot 역시 메인 애플리케이션 클래스를 프로젝트의 루트 패키지에 두는 것을 권장합니다. + +반대로 이런 구조라면 어떻게 될까요? + +```text +com.example.app +└── MyApplication.java + +com.example.service +└── UserService.java +``` + +`UserService`는 `MyApplication`의 하위 패키지가 아니기 때문에 기본 Component Scan으로 발견되지 않을 수 있습니다. +
+이런 경우에는 직접 범위를 지정할 수도 있습니다. + +```java +@ComponentScan(basePackages = "com.example") +@Configuration +public class AppConfig { +} +``` + +하지만 일반적인 Spring Boot 프로젝트에서는 메인 클래스를 최상위 패키지에 두는 것만으로 대부분의 문제를 피할 수 있습니다. + +이제 좀 더 깊이 들어가 볼까요? +`@ComponentScan` 내부에서는 무슨 일이 일어날까요? + +다음과 같은 코드가 있다고 해보겠습니다. + +```java +@Configuration +@ComponentScan("com.example") +public class AppConfig { +} +``` + +그리고 + +```java +@Service +public class UserService { +} +``` + +가 존재한다고 해볼까요? + +전체적인 과정은 다음과 같습니다. + +```text +Spring Container 시작 + ↓ +@Configuration 클래스 분석 + ↓ +@ComponentScan 발견 + ↓ +지정된 package 탐색 + ↓ +.class 파일 검색 + ↓ +Annotation metadata 분석 + ↓ +@Component 계열인지 확인 + ↓ +BeanDefinition 생성 + ↓ +BeanDefinitionRegistry에 등록 + ↓ +실제 Bean 생성 +``` + +이렇게 보니 너무너무 길어보이고, 정확히 어떤 뜻인지 모르겠으니, 하나씩 조금 더 자세히 살펴봅시다. + +#### ① Spring Container의 설정 클래스 분석 + +Spring이 ApplicationContext를 초기화하면서 설정 정보를 읽습니다. +이 과정에서 `@Configuration` 클래스 등을 처리하는 핵심 컴포넌트 중 하나가 `ConfigurationClassPostProcessor`입니다. +
+이 객체는 `@Configuration` 클래스를 처리하고, 그 내부의 `@ComponentScan`, `@Bean`, `@Import` 등의 설정을 분석하여 추가적인 BeanDefinition을 등록합니다. +`ConfigurationClassPostProcessor`는 Spring에서 Configuration 클래스를 부트스트랩하기 위한 `BeanFactoryPostProcessor`입니다. + +#### ② `@ComponentScan` 발견 + +설정 클래스를 분석하다가 다음을 발견합니다. + +```java +@ComponentScan("com.example") +``` + +그러면 Spring은 _"com.example 아래에서 Component 후보를 찾아야겠다"_ 라고 판단합니다. +이 과정에서 사용되는 핵심 클래스가 `ClassPathBeanDefinitionScanner`입니다. + +#### ③ Classpath 탐색 + +Scanner는 지정한 패키지를 Classpath상의 경로로 변환합니다. + +개념적으로, `com.example`이 `com/example` 형태로 변환되고, 그 아래의 `.class` 파일들을 탐색합니다. + +```text +com/example/UserService.class +com/example/OrderService.class +com/example/UserRepository.class +... +``` + +이때 Spring이 모든 클래스를 무조건 `Class.forName(...)`으로 로딩한 뒤 Reflection을 돌리는 것은 아닙니다. + +일반적인 classpath scanning 과정에서 Spring의 `ClassPathScanningCandidateComponentProvider`는 `MetadataReader`를 사용하며, 이 메타데이터 읽기 기능은 ASM의 `ClassReader`를 기반으로 동작합니다. + +쉽게 말하면, 클래스를 실제로 모두 객체화하거나 로딩해서 검사하기보다 `.class` 파일의 메타데이터를 읽어 어노테이션 등의 정보를 확인할 수 있다는 것입니다. + +그래서 + +```text +UserService.class + ↓ +ASM으로 클래스 메타데이터 읽기 + ↓ +@Service 발견 + ↓ +@Service 위의 @Component 확인 + ↓ +Bean 후보! +``` + +와 같은 과정이 가능합니다. + +#### ④ Bean 후보인지 필터링 + +`ClassPathBeanDefinitionScanner`는 모든 클래스를 Bean으로 등록하지 않습니다. + +기본적으로 `@Component` 또는 `@Component`를 Meta Annotation으로 가지고 있는 클래스들을 후보로 판단합니다. + +따라서, + +```java +public class NormalClass { +} +``` + +는 기본 스캔 대상이 아니지만 + +```java +@Component +public class ComponentClass { +} +``` + +나 + +```java +@Service +public class UserService { +} +``` + +는 후보가 됩니다. + +`@ComponentScan`에는 필터도 지정할 수 있습니다. + +```java +@ComponentScan( + basePackages = "com.example", + excludeFilters = { + @ComponentScan.Filter( + type = FilterType.ANNOTATION, + classes = Controller.class + ) + } +) +``` + +이런 식으로 특정 Component를 제외하거나 사용자 정의 조건에 맞는 클래스만 포함시킬 수도 있습니다. + +#### ⑤ BeanDefinition 만들기 + +Bean 후보를 찾았다고 바로 `new UserService();`를 실행하는 것은 아닙니다. + +먼저 `BeanDefinition`을 만듭니다. + +개념적으로는 다음과 같은 정보가 담긴다고 생각하면 됩니다. + +```text +BeanDefinition +────────────────────────── +Bean Class : UserService +Bean Name : userService +Scope : singleton +Lazy : false +Primary : false +Dependencies : ... +────────────────────────── +``` + +즉, "나중에 UserService 객체를 만들 때 이렇게 만들어라."라는 설계도라고 할 수 있겠네요. + +#### ⑥Bean 이름 결정 및 등록 + +Bean은 Container 안에서 이름도 가집니다. + +예를 들어 + +```java +@Service +public class UserService { +} +``` + +는 기본적인 이름 생성 규칙에 따라 보통 `userService`라는 이름으로 등록됩니다. + +직접 이름을 지정할 수도 있습니다. + +```java +@Service("customUserService") +public class UserService { +} +``` + +이 경우 Bean의 이름은 `customUserService`가 됩니다. + +그리고 최종적으로 BeanDefinition이 `BeanDefinitionRegistry`에 등록됩니다. + +여기까지가 크게 보면 **Bean 등록 과정**입니다. + +그럼 Bean은 언제 **실제 객체**가 되는걸까요? + +BeanDefinition이 모두 준비되었다면 이제 Spring Container는 실제 Bean을 생성합니다. +특히 기본 Scope인 `singleton` Bean은 일반적으로 ApplicationContext가 초기화되는 과정에서 미리 생성됩니다. +물론 `@Lazy`가 붙어 있다면 실제로 필요할 때까지 생성을 미룰 수도 있습니다. + +이제 드디어 **Bean Lifecycle**이 시작됩니다. + +### Bean Lifecycle + +Spring Bean도 태어나고, 살아가고, 죽습니다. + +전체적인 라이프사이클을 단순화하면 다음과 같습니다. + +> 인스턴스화 → 의존성 주입(DI) → 초기화 콜백(Initialization) → 빈 사용 → 소멸 콜백(Destruction) + +조금 더 자세하게 풀면 아래와 같다고 볼 수 있죠. + +```text +1. BeanDefinition 등록 + ↓ +2. 의존성 확인 + ↓ +3. Bean 객체 생성 + ↓ +4. 의존성 주입 + ↓ +5. 초기화 전 BeanPostProcessor + ↓ +6. @PostConstruct 등 초기화 + ↓ +7. 초기화 후 BeanPostProcessor + ↓ +8. Bean 사용 + ↓ +9. Container 종료 + ↓ +10. @PreDestroy 등 소멸 처리 +``` + +여기서 **BeanPostProcessor**는 무엇일까요? -### 2. Java 어노테이션(Annotation)과 구현 +`BeanPostProcessor`는 이름 그대로 "Bean을 생성하는 과정에 끼어들어 추가적인 처리를 할 수 있도록 해주는 확장 지점"입니다. -- 어노테이션은 프로그램 소스 코드에 추가할 수 있는 메타데이터(Metadata) -- 프로그램의 비즈니스 로직 자체에는 직접적인 영향을 주지 않지만, 컴파일러에게 정보를 제공하거나 런타임 시 특정 기능을 수행하도록 프레임워크에 힌트를 준다. -- **Java에서의 구현:** - - `@interface` 키워드를 사용하여 선언 - - **메타 어노테이션:** `@Target`(적용 대상: 클래스, 메서드, 필드 등)과 `@Retention`(유지 정책: Source, Class, Runtime)을 통해 동작 방식을 정의 - - 스프링과 같은 프레임워크는 주로 `@Retention(RetentionPolicy.RUNTIME)`으로 설정된 어노테이션을 **리플렉션(Reflection) API**를 통해 런타임에 읽어들여 부가적인 처리를 수행 +대표적으로 다음 두 메서드를 제공합니다. + +```java +public interface BeanPostProcessor { + + default Object postProcessBeforeInitialization( + Object bean, + String beanName) { + return bean; + } + + default Object postProcessAfterInitialization( + Object bean, + String beanName) { + return bean; + } +} +``` + +즉, + +```text +Bean 생성 + ↓ +BeanPostProcessor +Before Initialization + ↓ +초기화 + ↓ +BeanPostProcessor +After Initialization + ↓ +Bean 사용 +``` + +같은 구조가 됩니다. + +Spring 공식 문서에서도 `BeanPostProcessor`가 Bean의 초기화 콜백 전후에 호출되며, Bean을 검사하거나 심지어 프록시 객체로 감싸 반환할 수도 있다고 설명합니다. +실제 Spring AOP 인프라의 일부 역시 이러한 `BeanPostProcessor` 메커니즘을 활용합니다. + +어, 근데! 어디서 많이 본 것 같지 않나요? +
+앞서 AOP를 공부할 때 실제 객체를 Proxy로 감싼다는 이야기를 했습니다. +
+바로 이런 Spring의 확장 지점 덕분에 Bean 생성 과정에서 Proxy 적용 같은 추가 작업을 수행할 수 있는 것입니다. + +### Bean Scope + +Bean Lifecycle을 이해했다면 이번에는 **Bean이 얼마나 오래 살아있는지** 알아볼까요? +
+이걸 결정하는 것이 바로 **Bean Scope**입니다. + +Scope는 쉽게 말하면, +
+_"Bean 객체를 몇 개 만들고, 어느 범위까지 공유할까?"_ +
+를 결정합니다. + +Spring이 제공하는 대표적인 Scope는 다음과 같습니다. + +| Scope | 의미 | +| ------------- | ------------------------------------------------ | +| `singleton` | 하나의 Spring Container에서 BeanDefinition 하나당 하나의 객체 | +| `prototype` | Bean을 요청할 때마다 새로운 객체 | +| `request` | HTTP Request 하나당 하나 | +| `session` | HTTP Session 하나당 하나 | +| `application` | ServletContext 하나당 하나 | +| `websocket` | WebSocket 하나당 하나 | + +Spring Framework는 이 여섯 종류의 기본 Scope를 제공하며, `request`, `session`, `application`, `websocket`은 Web-aware ApplicationContext에서 사용할 수 있습니다. + +몇가지 더 자세히 알아볼까요? + +#### ① singleton + +Spring Bean의 기본 Scope입니다. +아무것도 지정하지 않으면 기본적으로 singleton이 적용됩니다. + +```java +@Service +public class UserService { +} +``` + +라는 코드는 개념적으로는 + +```java +@Scope("singleton") +@Service +public class UserService { +} +``` + +와 비슷합니다. + +예를 들어 + +```java +UserService userService1 = context.getBean(UserService.class); +UserService userService2 = context.getBean(UserService.class); +``` -### 3. 스프링의 어노테이션 기반 Bean 등록 과정 +라고 하면 일반적인 singleton Bean에서는 `userService1 == userService2`가 `true`인거죠. +즉, 여러 곳에서 동일한 Bean을 공유하는 것입니다. -스프링에서 `@Component`나 `@Bean` 어노테이션을 사용해 빈을 등록할 때 컨테이너 내부에서 일어나는 과정 +여기서 주의해야 할 점은, singleton이라고 해서 Thread-safe한 것이 아니라는 것입니다. -1. **메타데이터 읽기:** 설정 클래스(예: `@Configuration`이 붙은 클래스) 읽기 -2. **BeanDefinition 생성:** 컨테이너는 즉시 객체를 생성하지 않고, 어노테이션 정보를 바탕으로 빈의 설계도격인 **`BeanDefinition`** 객체를 생성. 여기에는 빈의 클래스 이름, 스코프, 초기화 메서드, 의존성 정보 등이 담김. -3. **레지스트리 등록:** 생성된 `BeanDefinition`을 컨테이너 내부의 `BeanDefinitionRegistry`에 등록 -4. **인스턴스화 및 의존성 주입:** 빈 생성이 필요한 시점(싱글톤의 경우 컨테이너 초기화 시점)에 `BeanDefinition`을 기반으로 실제 Java 객체를 생성(Reflection 활용)하고, 필요한 의존성을 주입(DI) +예를 들어 + +```java +@Service +public class CounterService { + + private int count = 0; + + public void increase() { + count++; + } +} +``` + +같은 mutable state를 singleton Bean 안에 두면 여러 요청이 동시에 같은 객체를 사용하면서 동시성 문제가 발생할 수 있습니다. +
+그래서 일반적인 Service Bean은 가능한 한 **상태를 가지지 않는 Stateless 객체**로 설계하는 것이 좋습니다. + +#### ② prototype + +Prototype Scope는 Bean을 요청할 때마다 새로운 객체를 생성합니다. + +```java +@Component +@Scope("prototype") +public class PrototypeBean { +} +``` + +그리고 + +```java +PrototypeBean bean1 = + context.getBean(PrototypeBean.class); + +PrototypeBean bean2 = + context.getBean(PrototypeBean.class); +``` + +를 실행한다면 `bean1 == bean2`는 `false`가 되겠죠. +
+Spring 공식 문서에서도 prototype Bean은 해당 Bean이 요청될 때마다 새로운 인스턴스를 생성한다고 설명합니다. + +그런데 prototype에는 아주 중요한 특징이 있습니다. +
+Spring은 Prototype Bean의 **생성, 의존성 주입, 초기화**까지는 관리하지만, +Bean을 사용자에게 넘겨준 이후의 **완전한 lifecycle까지 계속 추적하지 않습니다.** +따라서 prototype Bean의 destruction callback은 컨테이너가 자동으로 호출해주지 않습니다. 자원 정리가 필요한 Prototype Bean이라면 이를 직접 처리해야 합니다. + +#### ③ request + +Web Application에서 사용할 수 있는 Scope입니다. + +```java +@Component +@RequestScope +public class RequestInfo { +} +``` + +HTTP Request 하나마다 별도의 Bean이 만들어집니다. + +```text +Request A ──→ RequestInfo A +Request B ──→ RequestInfo B +Request C ──→ RequestInfo C +``` + +한 Request 내부에서는 같은 객체를 사용하지만 다른 Request와는 공유하지 않습니다. + +#### ④ session + +HTTP Session 하나마다 Bean을 생성합니다. + +```java +@Component +@SessionScope +public class UserSession { +} +``` + +로그인 사용자별 상태 등을 관리하는 상황을 떠올릴 수 있겠네요. +다만 서버의 세션 상태 관리 방식과 확장성을 고려해야 하므로 무분별하게 사용하는 것은 피하는 것이 좋습니다. + +### 정리 + +그럼 우리 지금까지 배운 내용을 한 번에 이어볼까요? +어노테이션으로 Bean이 등록되는 전체 과정을 다시 연결해봅시다. + +```java +@Service +public class UserService { + + private final UserRepository userRepository; + + public UserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @PostConstruct + public void init() { + System.out.println("UserService 준비 완료!"); + } +} +``` + +그리고 애플리케이션을 실행합니다. + +```java +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +``` + +그러면 개념적으로 아래와 같은 과정이 벌어집니다. + +```text +SpringApplication.run() + ↓ +ApplicationContext 생성 + ↓ +@SpringBootApplication 분석 + ↓ +@ComponentScan 발견 + ↓ +base package 결정 + ↓ +classpath 탐색 + ↓ +UserService.class 발견 + ↓ +ASM / MetadataReader로 +annotation metadata 확인 + ↓ +@Service 발견 + ↓ +@Service의 meta annotation인 +@Component 확인 + ↓ +Bean Candidate 결정 + ↓ +UserService용 BeanDefinition 생성 + ↓ +BeanDefinitionRegistry 등록 + ↓ +singleton Bean 생성 시점 + ↓ +UserService 생성자 확인 + ↓ +UserRepository Bean 탐색 + ↓ +UserRepository 주입 + ↓ +UserService 객체 생성 + ↓ +BeanPostProcessor 처리 + ↓ +@PostConstruct 호출 + ↓ +필요한 후처리 및 Proxy 생성 + ↓ +Singleton Bean으로 관리 + ↓ +애플리케이션에서 사용 + ↓ +ApplicationContext 종료 + ↓ +@PreDestroy 등의 소멸 처리 +``` + +처음에는 `@Service` 한 줄밖에 안 썼는데 그 뒤에서는 이렇게 많은 일이 벌어지고 있었던 것입니다. + +### @Bean + +근데, 우리는 `@Bean`이라는 어노테이션도 알고 있습니다. +`@Bean`으로 등록하는 것은 어떻게 다를까요? + +Bean을 등록하는 방법이 Component Scan만 있는 것은 아닙니다. +우리는 다음과 같이 직접 Bean을 등록할 수도 있습니다. + +```java +@Configuration +public class AppConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} +``` + +`@Component` 방식은 "Spring아, 이 패키지를 뒤져서 Bean 후보를 찾아줘" 에 가깝다면, +`@Bean` 방식은 "Spring아, 이 메서드가 반환하는 객체를 Bean으로 등록해줘" 에 가깝습니다. + +Spring 공식 문서에서도 `@Bean`은 해당 메서드가 생성하고 구성한 객체를 Spring IoC Container가 관리하도록 정의하는 방법이며, `@Configuration`은 Bean Definition의 소스 역할을 하는 클래스라고 설명합니다. + +그래서 우리가 직접 만든 클래스에는 보통 `@Service`, `@Repository`, `@Controller`, `@Component` 등을 사용하고, +외부 라이브러리 객체처럼 우리가 해당 클래스 소스에 직접 `@Component`를 붙일 수 없는 경우에는 `@Bean`을 이용해서 등록하는 경우가 많습니다. + +### 🤔 하나의 Interface를 구현한 Service가 여러 개라면? + +마지막으로 실무에서 자주 만나게 되는 상황을 살펴봅시다. + +결제 기능이 있다고 해볼까요? + +```java +public interface PaymentService { + + void pay(); +} +``` + +그리고 구현체가 두 개 있습니다. + +```java +@Service +public class KakaoPaymentService + implements PaymentService { + + @Override + public void pay() { + System.out.println("카카오페이 결제"); + } +} +``` + +```java +@Service +public class NaverPaymentService + implements PaymentService { + + @Override + public void pay() { + System.out.println("네이버페이 결제"); + } +} +``` + +이제 다음 코드를 작성하면 어떻게 될까요? + +```java +@Service +public class OrderService { + + private final PaymentService paymentService; + + public OrderService(PaymentService paymentService) { + this.paymentService = paymentService; + } +} +``` + +Spring 입장에서는 하나의 타입에 여러 Bean이 후보가 되기 때문에 PaymentService를 달라고 요청 받았을 때, 뭘 넣어줘야 하는지 난감해지겠죠. + +Spring 공식 문서에서도 type 기반 Autowiring에서 후보가 여러 개 존재할 경우 추가적인 선택 기준이 필요하다고 설명합니다. + +#### ① `@Primary` + +이 방법은 기본으로 사용할 구현체를 하나 지정합니다. + +```java +@Primary +@Service +public class KakaoPaymentService + implements PaymentService { +} +``` + +이제 + +```java +public OrderService(PaymentService paymentService) { + this.paymentService = paymentService; +} +``` + +라고 하면 여러 후보 중 `@Primary`가 붙은 `KakaoPaymentService`가 우선적으로 선택됩니다. + +#### ② `@Qualifier` + +특정 구현체를 명확하게 선택하고 싶다면 `@Qualifier`를 사용할 수 있습니다. + +```java +@Service +public class OrderService { + + private final PaymentService paymentService; + + public OrderService( + @Qualifier("kakaoPaymentService") + PaymentService paymentService + ) { + this.paymentService = paymentService; + } +} +``` + +Spring은 `PaymentService` 타입 후보 중 qualifier 조건에 맞는 Bean을 좁혀 선택합니다. + +조금 더 의미 있는 이름을 직접 지정할 수도 있습니다. + +```java +@Service +@Qualifier("kakao") +public class KakaoPaymentService + implements PaymentService { +} +``` + +```java +@Service +public class OrderService { + + private final PaymentService paymentService; + + public OrderService( + @Qualifier("kakao") + PaymentService paymentService + ) { + this.paymentService = paymentService; + } +} +``` + +#### ③ 구현체를 전부 주입받기 + +Spring은 같은 Interface를 구현한 Bean들을 한 번에 주입할 수도 있습니다. + +```java +@Service +public class PaymentManager { + + private final List paymentServices; + + public PaymentManager( + List paymentServices + ) { + this.paymentServices = paymentServices; + } +} +``` + +이렇게 하면 `PaymentService`를 구현한 Bean들이 Collection으로 들어옵니다. + +또는 `Map`으로 받을 수도 있습니다. + +```java +@Service +public class PaymentManager { + + private final Map paymentServices; + + public PaymentManager( + Map paymentServices + ) { + this.paymentServices = paymentServices; + } + + public void pay(String type) { + paymentServices.get(type).pay(); + } +} +``` -### 4. `@ComponentScan`의 컴포넌트 탐색 과정 심층 분석 +### 정리하면, -`@ComponentScan`: 스프링이 어디서부터 컴포넌트를 찾을지(`basePackages`) 지정하고, 이를 자동으로 빈으로 등록하는 기능 +- **Spring Bean**은 Spring IoC Container가 생성하고 관리하는 객체입니다. +- **Annotation**은 그 자체가 어떤 기능을 실행하는 명령어가 아니라, 프로그램이 읽을 수 있는 Metadata입니다. +- `@ComponentScan`은 Classpath를 탐색하여 `@Component` 또는 이를 Meta Annotation으로 가지고 있는 클래스들을 찾아 BeanDefinition으로 등록합니다. +- **Bean Lifecycle**은 단순화하면, *객체 생성 → 의존성 주입 → 초기화 → BeanPostProcessor → 사용 → 소멸*의 흐름을 가집니다. +- **Bean Scope**는 해당 Bean 객체를 어느 범위까지 공유할 것인지를 결정합니다. -1. **클래스패스 스캐닝 (Classpath Scanning):** `ClassPathBeanDefinitionScanner`가 지정된 베이스 패키지와 그 하위 패키지의 디렉토리를 파일 시스템이나 JAR 파일 내의 클래스패스에서 스캔 -2. **ASM을 통한 바이트코드 분석 (클래스 로딩 방지):** 스프링은 스캔 과정에서 메모리 낭비와 부작용을 막기 위해 런타임에 클래스를 직접 메모리에 로딩(Class Loading)하지 않고 대신 ASM이라는 바이트코드 조작 라이브러리를 기반으로 한 `MetadataReader`를 사용하여 `.class` 파일의 메타데이터만 빠르게 읽음 -3. **필터링 (Filtering):** 읽어들인 메타데이터 중 `@Component` 어노테이션(또는 이를 포함한 `@Service`, `@Repository`, `@Controller` 등의 메타 어노테이션)이 존재하는지 확인 -4. **BeanDefinition 등록:** 필터를 통과한 클래스들에 대해 `BeanDefinition`을 생성하고 레지스트리에 등록하여 이후 컨테이너가 빈으로 관리하게 함 +결국 Spring Bean을 제대로 이해한다는 것은 +"Spring Container가 어떤 정보를 바탕으로 객체를 발견하고, 생성하고, 연결하고, 관리하는가?" +를 이해하는 것이라고 볼 수 있겠네요! ---