<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>MUTO 와 함께 개발을</title>
    <link>https://leemoono.tistory.com/</link>
    <description>무식하게 공부하자.
토 나올 때 까지.</description>
    <language>ko</language>
    <pubDate>Tue, 14 Jul 2026 17:22:19 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>무토(MUTO)</managingEditor>
    <item>
      <title>Spring Boot Executable JAR에서 JSP 접근 불가 문제 해결</title>
      <link>https://leemoono.tistory.com/38</link>
      <description>&lt;h1&gt;Spring Boot Executable JAR에서 JSP 접근 불가 문제 해결&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Framework 2.5.6 기반의 레거시 시스템을 Spring Boot 2.7.18로 업그레이드하는 작업을 진행했다. 기존 시스템은 EAR 파일로 패키징하여 JavaEE 스펙인 외부 JEUS에 배포하는 방식이었는데, Spring Boot로 전환하면서 Executable JAR(&lt;code&gt;bootJar&lt;/code&gt;) 형태로 패키징해야 했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Executable JAR를 선택한 이유&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부 WAS 의존성 제거 (내장 Tomcat 사용)&lt;/li&gt;
&lt;li&gt;배포 단순화 (&lt;code&gt;java -jar&lt;/code&gt;로 실행)&lt;/li&gt;
&lt;li&gt;컨테이너 환경(Docker/Kubernetes) 대응 용이&lt;/li&gt;
&lt;li&gt;운영 환경 표준화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 레거시 시스템에는 수백 개의 JSP 파일이 존재했고, 이를 당장 Thymeleaf 등으로 마이그레이션하는 것은 현실적으로 불가능했다. JSP를 유지하면서 Executable JAR로 패키징해야 하는 상황이었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 애플리케이션을 &lt;code&gt;bootJar&lt;/code&gt;로 패키징한 후 실행했더니 JSP 페이지에 접근할 수 없었다. 분명 &lt;code&gt;src/main/webapp&lt;/code&gt; 디렉토리에 JSP 파일들이 존재하는데, 404 에러가 발생했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
...
There was an unexpected error (type=Not Found, status=404).&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 ViewResolver 설정 문제인 줄 알았다. &lt;code&gt;application.properties&lt;/code&gt;에서 prefix/suffix 설정을 확인해봤지만 정상이었다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IDE에서 실행할 때는 잘 되는데, JAR로 패키징하면 안 되는 상황이었다. &quot;그러면 Gradle에서 webapp 파일을 JAR에 포함시키면 되지 않을까?&quot; 생각했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인 분석&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Executable JAR의 구조적 한계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot의 Executable JAR는 일반적인 WAR 파일과 구조가 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;WAR 파일 구조:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;myapp.war
├── WEB-INF/
│   ├── classes/
│   ├── lib/
│   └── views/
│       └── index.jsp
├── META-INF/
└── index.jsp&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring Boot Executable JAR 구조:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;myapp.jar
├── BOOT-INF/
│   ├── classes/
│   │   └── (컴파일된 클래스들)
│   └── lib/
│       └── (의존성 JAR들)
├── META-INF/
│   └── MANIFEST.MF
└── org/springframework/boot/loader/
    └── (Spring Boot Loader 클래스들)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제의 핵심은 &lt;b&gt;&lt;code&gt;src/main/webapp&lt;/code&gt; 디렉토리가 Executable JAR에 포함되지 않는다&lt;/b&gt;는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Gradle의 기본 동작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gradle의 &lt;code&gt;bootJar&lt;/code&gt; 태스크는 다음 위치만 JAR에 포함한다:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;소스 위치&lt;/th&gt;
&lt;th&gt;JAR 내 위치&lt;/th&gt;
&lt;th&gt;포함 여부&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/main/java&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BOOT-INF/classes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/main/resources&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BOOT-INF/classes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/main/webapp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;&lt;b&gt;X&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;webapp&lt;/code&gt; 디렉토리는 WAR 패키징 전용이다. JAR 패키징 시에는 완전히 무시된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Gradle 복사만으로 해결되지 않는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;그러면 Gradle에서 &lt;code&gt;src/main/webapp&lt;/code&gt;을 &lt;code&gt;META-INF/resources&lt;/code&gt;로 복사하면 되지 않을까?&quot; 생각했다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;task copyWebappToResources(type: Copy) {
    from 'src/main/webapp'
    into 'build/resources/main/META-INF/resources'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 JAR 내부에 JSP 파일이 포함된다:&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;myapp.jar
└── BOOT-INF/
    └── classes/
        └── META-INF/
            └── resources/
                └── WEB-INF/
                    └── views/
                        └── index.jsp&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그런데 이것만으로는 동작하지 않았다.&lt;/b&gt; 이유는 다음과 같다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Servlet 스펙의 제약&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Servlet 스펙에 따르면 &lt;code&gt;META-INF/resources&lt;/code&gt;는 &lt;b&gt;&lt;code&gt;WEB-INF/lib&lt;/code&gt;에 패키징된 JAR에서만&lt;/b&gt; 로드되어야 한다. 주 아카이브(메인 JAR) 내의 &lt;code&gt;META-INF/resources&lt;/code&gt;에서는 로드되면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 1.4.3부터 이 스펙을 엄격히 준수하기 시작했다. 따라서 &lt;code&gt;BOOT-INF/classes/META-INF/resources&lt;/code&gt;에 있는 리소스는 Tomcat이 자동으로 인식하지 않는다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 경로 불일치&lt;/h4&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────┐
│ Gradle 복사 결과                                             │
│ &amp;rarr; BOOT-INF/classes/META-INF/resources/WEB-INF/views/*.jsp  │
└─────────────────────────────────────────────────────────────┘
                              &amp;ne;
┌─────────────────────────────────────────────────────────────┐
│ Tomcat이 탐색하는 위치 (Servlet 스펙 기준)                   │
│ &amp;rarr; WEB-INF/lib/*.jar 내부의 META-INF/resources              │
└─────────────────────────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tomcat은 &lt;code&gt;BOOT-INF/classes&lt;/code&gt; 안에 있는 &lt;code&gt;META-INF/resources&lt;/code&gt;를 웹 리소스로 인식하지 않는다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. ClassLoader 계층 구조 문제&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot의 Executable JAR는 특수한 ClassLoader 구조를 사용한다:&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;┌─────────────────────────────────────────┐
│ AppClassLoader                          │
│ URL: file:/path/to/myapp.jar            │
│ (fat jar 자체만 포함)                    │
└─────────────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────┐
│ LaunchedURLClassLoader                  │
│ URL: jar:file:/path/to/myapp.jar!/      │
│      BOOT-INF/classes/                  │
│      BOOT-INF/lib/*.jar                 │
└─────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;AbstractEmbeddedServletContainerFactory.getUrlsOfJarsWithMetaInfResources()&lt;/code&gt; 메서드는 ClassLoader의 URL을 순회하며 &lt;code&gt;META-INF/resources&lt;/code&gt;가 있는 JAR를 찾는다. 그러나:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;AppClassLoader&lt;/b&gt;는 fat jar URL만 반환한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LaunchedURLClassLoader&lt;/b&gt;는 &lt;code&gt;BOOT-INF&lt;/code&gt; 내부 경로만 포함한다&lt;/li&gt;
&lt;li&gt;이 메서드는 부모 ClassLoader를 탐색하지 않는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 &lt;code&gt;BOOT-INF/classes/META-INF/resources&lt;/code&gt;는 Tomcat에 등록되지 않는다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. Spring Boot 팀의 공식 입장&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 &lt;a href=&quot;https://github.com/spring-projects/spring-boot/issues/8324&quot;&gt;GitHub Issue #8324&lt;/a&gt;에서 논의되었다. Spring Boot 팀의 Andy Wilkinson은 다음과 같이 설명했다:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Servlet 스펙은 &lt;code&gt;META-INF/resources&lt;/code&gt;의 리소스가 &lt;code&gt;WEB-INF/lib&lt;/code&gt;에 패키징된 JAR에서만 로드되어야 하며, 주 아카이브 내의 &lt;code&gt;META-INF/resources&lt;/code&gt;에서는 로드되지 않아야 한다고 명시하고 있습니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 이슈는 &lt;b&gt;&quot;status: declined&quot;&lt;/b&gt;로 종료되었다. Spring Boot 팀은 &lt;code&gt;BOOT-INF/classes/META-INF/resources&lt;/code&gt; 지원을 공식적으로 거부했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결/대응 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gradle 복사만으로는 부족하다. 두 가지 전략을 &lt;b&gt;함께&lt;/b&gt; 적용해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전략 1: META-INF/resources로 복사 (필요조건)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 JSP 파일을 JAR 내부에 포함시켜야 한다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;// webapp 디렉토리를 META-INF/resources로 복사
task copyWebappToResources(type: Copy) {
    from 'src/main/webapp'
    into 'build/resources/main/META-INF/resources'
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

// processResources가 webapp 복사 이후에 실행되도록
processResources {
    dependsOn copyWebappToResources
}

// bootJar 설정
bootJar {
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의&lt;/b&gt;: 이것만으로는 부족하다! 위에서 설명한 대로 Tomcat이 이 경로를 자동으로 인식하지 않기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전략 2: TomcatServletWebServerFactory 커스터마이징 (충분조건)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tomcat에 명시적으로 리소스 경로를 등록해야 한다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;@Configuration
public class JspConfiguration {

    @Bean
    public WebServerFactoryCustomizer&amp;lt;TomcatServletWebServerFactory&amp;gt; tomcatCustomizer() {
        return factory -&amp;gt; factory.addContextCustomizers(context -&amp;gt; {
            // JAR 스캔 필터 설정
            context.getJarScanner().setJarScanFilter((jarScanType, jarName) -&amp;gt; true);

            WebResourceRoot resources = new StandardRoot(context);

            try {
                URL resourceUrl = getClass().getClassLoader()
                    .getResource(&quot;META-INF/resources&quot;);

                if (resourceUrl != null) {
                    String protocol = resourceUrl.getProtocol();

                    if (&quot;jar&quot;.equals(protocol)) {
                        // JAR 내부 리소스 등록
                        String jarPath = resourceUrl.toString();
                        int separatorIndex = jarPath.indexOf(&quot;!/&quot;);
                        if (separatorIndex &amp;gt; 0) {
                            // jar:file:/path/to/file.jar!/META-INF/resources
                            String jarFileUrl = jarPath.substring(4, separatorIndex);
                            jarFileUrl = jarFileUrl.substring(5); // &quot;file:&quot; 제거

                            resources.addPreResources(
                                new JarResourceSet(resources, &quot;/&quot;,
                                    jarFileUrl, &quot;/META-INF/resources&quot;)
                            );
                        }
                    } else {
                        // IDE 실행 시 (파일 시스템)
                        File resourceFile = new File(resourceUrl.toURI());
                        resources.addPreResources(
                            new DirResourceSet(resources, &quot;/&quot;,
                                resourceFile.getAbsolutePath(), &quot;/&quot;)
                        );
                    }
                }
            } catch (Exception e) {
                throw new RuntimeException(
                    &quot;Failed to configure JSP resources&quot;, e);
            }

            context.setResources(resources);
        });
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정이 하는 일:&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────┐
│ 요청: /WEB-INF/views/index.jsp                              │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│ Tomcat WebResourceRoot                                      │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ PreResources (우선 탐색)                                 │ │
│ │ &amp;rarr; JarResourceSet: JAR!/META-INF/resources/              │ │
│ │   또는                                                   │ │
│ │ &amp;rarr; DirResourceSet: 파일시스템 경로                        │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│ 매핑: / &amp;rarr; META-INF/resources/                               │
│ 결과: META-INF/resources/WEB-INF/views/index.jsp 찾음       │
└─────────────────────────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;두 전략이 모두 필요한 이유&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;전략&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;th&gt;단독 적용 시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;전략 1 (Gradle 복사)&lt;/td&gt;
&lt;td&gt;JAR 내부에 JSP 파일 포함&lt;/td&gt;
&lt;td&gt;Tomcat이 인식 못함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;전략 2 (Tomcat 설정)&lt;/td&gt;
&lt;td&gt;Tomcat에 리소스 경로 등록&lt;/td&gt;
&lt;td&gt;파일이 없어서 404&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;전략 1&lt;/b&gt;은 필요조건이다. JSP 파일이 JAR 안에 있어야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전략 2&lt;/b&gt;는 충분조건이다. Tomcat이 해당 경로를 인식하도록 명시적으로 등록해야 한다.&lt;/li&gt;
&lt;li&gt;둘 다 있어야 동작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전략 선택 가이드&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;JSP를 Executable JAR에서 사용해야 하는가?
    │
    ├─ YES &amp;rarr; 전략 1 + 전략 2 모두 적용
    │
    └─ NO &amp;rarr; WAR 패키징 또는 Thymeleaf 등 템플릿 엔진 사용 권장&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의&lt;/b&gt;: Spring Boot 공식 문서에서는 Executable JAR + JSP 조합을 권장하지 않는다. 레거시 시스템이 아니라면 Thymeleaf 등 템플릿 엔진으로 마이그레이션하는 것이 장기적으로 유리하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot Executable JAR에서 JSP가 동작하지 않는 이유는 &lt;b&gt;Servlet 스펙과 Spring Boot의 중첩 JAR 구조 때문&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 인사이트:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Gradle로 &lt;code&gt;META-INF/resources&lt;/code&gt;에 복사하는 것만으로는 부족하다&lt;/li&gt;
&lt;li&gt;Servlet 스펙상 &lt;code&gt;BOOT-INF/classes/META-INF/resources&lt;/code&gt;는 웹 리소스로 인식되지 않는다&lt;/li&gt;
&lt;li&gt;Spring Boot 팀은 이 동작을 의도적으로 설계했으며, 지원 요청을 거부했다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;JarResourceSet&lt;/code&gt;으로 Tomcat에 명시적으로 리소스 경로를 등록해야 한다&lt;/li&gt;
&lt;li&gt;Gradle 복사(필요조건) + Tomcat 설정(충분조건) 두 가지를 함께 적용해야 동작한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#web.servlet.embedded-container.jsp-limitations&quot;&gt;Spring Boot Reference - JSP Limitations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tomcat.apache.org/tomcat-9.0-doc/api/org/apache/catalina/WebResourceRoot.html&quot;&gt;Apache Tomcat - WebResourceRoot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.gradle.org/current/dsl/org.gradle.api.tasks.Copy.html&quot;&gt;Gradle - Copy Task&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-boot/issues/9525&quot;&gt;GitHub Issue #9525 - Static resources from META-INF/resources not made available to Tomcat&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-boot/issues/8324&quot;&gt;GitHub Issue #8324 - Configure embedded containers to load static resources from fat jar's BOOT-INF/classes/META-INF/resources&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Java&amp;amp;Spring/차세대를 하면서...</category>
      <author>무토(MUTO)</author>
      <guid isPermaLink="true">https://leemoono.tistory.com/38</guid>
      <comments>https://leemoono.tistory.com/38#entry38comment</comments>
      <pubDate>Tue, 9 Dec 2025 18:41:29 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot 애플리케이션 실행 시 JAR 스캔 경고 문제 분석</title>
      <link>https://leemoono.tistory.com/37</link>
      <description>&lt;h1&gt;Spring Boot 애플리케이션 실행 시 JAR 스캔 경고 문제 분석&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 애플리케이션을 &lt;code&gt;gradle bootRun&lt;/code&gt;으로 실행하던 중 다음과 같은 경고 로그가 발생했다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;2025-12-08 19:07:45.775 # WARN 40868 --- [main] o.a.t.u.s.StandardJarScanner:
Failed to scan [file:/C:/Users/USER/.m2/repository/com/sun/xml/ws/jaxws-rt/2.3.1/javax.annotation-api.jar] from classloader hierarchy

java.io.IOException: java.lang.reflect.InvocationTargetException
    at org.apache.tomcat.util.compat.Jre9Compat.jarFileNewInstance(Jre9Compat.java:209)
    at org.apache.tomcat.util.scan.JarFileUrlJar.&amp;lt;init&amp;gt;(JarFileUrlJar.java:65)
    at org.apache.tomcat.util.scan.StandardJarScanner.process(StandardJarScanner.java:387)
    ...
Caused by: java.nio.file.NoSuchFileException:
C:\Users\USER\.m2\repository\com\sun\xml\ws\jaxws-rt\2.3.1\javax.annotation-api.jar&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 패턴으로 &lt;code&gt;javax.jws-api.jar&lt;/code&gt;, &lt;code&gt;jaxb-api.jar&lt;/code&gt; 파일도 찾을 수 없다는 경고가 함께 출력됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Maven 로컬 저장소가 손상됐거나, 의존성 다운로드가 실패한 줄 알았다. 그래서 해당 디렉토리를 삭제하고 다시 빌드해봤지만 동일한 경고가 계속 발생했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인 분석&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제의 핵심은 &lt;b&gt;JAR 파일이 없는 게 아니라, 잘못된 경로에서 찾고 있다&lt;/b&gt;는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 자세히 보면 Tomcat의 &lt;code&gt;StandardJarScanner&lt;/code&gt;가 &lt;code&gt;jaxws-rt-2.3.1&lt;/code&gt; 디렉토리 안에서 &lt;code&gt;javax.annotation-api.jar&lt;/code&gt;를 찾고 있다. 하지만 실제로 이 파일은 Maven 저장소의 완전히 다른 좌표에 존재한다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;# Tomcat이 찾는 경로 (잘못됨)
C:\Users\USER\.m2\repository\com\sun\xml\ws\jaxws-rt\2.3.1\javax.annotation-api.jar

# 실제 파일이 있는 경로
C:\Users\USER\.m2\repository\javax\annotation\javax.annotation-api\1.3.2\javax.annotation-api-1.3.2.jar&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이런 일이 발생하는 걸까?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MANIFEST.MF의 Class-Path 속성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;jaxws-rt-2.3.1.jar&lt;/code&gt; 파일의 &lt;code&gt;MANIFEST.MF&lt;/code&gt;를 열어보면 다음과 같은 내용이 있다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;Manifest-Version: 1.0
Class-Path: javax.annotation-api.jar javax.jws-api.jar jaxb-api.jar
 jaxb-core.jar jaxb-impl.jar stax-ex.jar ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;J2EE 스펙 기반 라이브러리들은 이렇게 &lt;code&gt;Class-Path&lt;/code&gt; 속성에 &lt;b&gt;상대 경로&lt;/b&gt;로 의존 라이브러리를 명시한다. 이 방식은 전통적인 Java EE 애플리케이션 서버 환경에서는 정상 동작한다. WAS가 공통 라이브러리들을 같은 디렉토리에 배치하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;code&gt;gradle bootRun&lt;/code&gt;으로 실행하면 상황이 달라진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;gradle bootRun의 클래스 로딩 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;bootRun&lt;/code&gt; 태스크는 Maven 로컬 저장소에 있는 JAR 파일들을 &lt;b&gt;개별적으로&lt;/b&gt; 클래스패스에 추가한다.&lt;/p&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;클래스패스 구성:
- C:\Users\USER\.m2\repository\com\sun\xml\ws\jaxws-rt\2.3.1\jaxws-rt-2.3.1.jar
- C:\Users\USER\.m2\repository\javax\annotation\javax.annotation-api\1.3.2\javax.annotation-api-1.3.2.jar
- C:\Users\USER\.m2\repository\javax\jws\javax.jws-api\1.1\javax.jws-api-1.1.jar
- ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 JAR는 자신의 Maven 좌표에 해당하는 디렉토리에 그대로 있다. 이 상태에서 Tomcat의 &lt;code&gt;StandardJarScanner&lt;/code&gt;가 &lt;code&gt;jaxws-rt-2.3.1.jar&lt;/code&gt;의 MANIFEST.MF를 읽고, Class-Path에 명시된 &lt;code&gt;javax.annotation-api.jar&lt;/code&gt;를 &lt;b&gt;동일 디렉토리에서&lt;/b&gt; 찾으려고 시도한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연히 그 위치에는 파일이 없으니 &lt;code&gt;NoSuchFileException&lt;/code&gt;이 발생하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;gradle bootRun 실행 시 클래스 로딩 구조:

Application ClassLoader
    └── jaxws-rt-2.3.1.jar (Maven 저장소 경로)
        └── MANIFEST.MF Class-Path 해석
            └── javax.annotation-api.jar를 동일 디렉토리에서 탐색
                └── 파일 없음 &amp;rarr; NoSuchFileException (WARN)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;java -jar 실행 시에는 왜 문제가 없을까&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신기하게도 &lt;code&gt;gradle bootJar&lt;/code&gt;로 빌드한 후 &lt;code&gt;java -jar&lt;/code&gt;로 실행하면 이 경고가 발생하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot의 실행 가능한 JAR(Executable JAR) 내부 구조를 보면 이유를 알 수 있다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;application.jar
├── BOOT-INF/
│   ├── classes/
│   │   └── (애플리케이션 코드)
│   └── lib/
│       ├── jaxws-rt-2.3.1.jar
│       ├── javax.annotation-api-1.3.2.jar
│       ├── javax.jws-api-1.1.jar
│       ├── jaxb-api-2.3.1.jar
│       └── (모든 의존성 라이브러리)
├── META-INF/
│   └── MANIFEST.MF
└── org/springframework/boot/loader/
    └── (Spring Boot 로더 클래스들)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot는 패키징 과정에서 모든 의존성 라이브러리를 &lt;code&gt;BOOT-INF/lib/&lt;/code&gt; 디렉토리에 &lt;b&gt;평탄화(flatten)&lt;/b&gt; 해서 배치한다. Maven 저장소의 복잡한 디렉토리 구조가 사라지고, 모든 JAR가 동일한 계층에 나란히 놓이게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;code&gt;java -jar&lt;/code&gt;로 실행하면 Spring Boot의 &lt;code&gt;LaunchedURLClassLoader&lt;/code&gt;가 동작한다. 이 클래스로더는 개별 JAR의 MANIFEST.MF에 있는 Class-Path 속성을 &lt;b&gt;무시&lt;/b&gt;하고, &lt;code&gt;BOOT-INF/lib/&lt;/code&gt; 전체를 하나의 클래스패스로 관리한다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;java -jar 실행 시 클래스 로딩 구조:

LaunchedURLClassLoader (Spring Boot 전용)
    └── BOOT-INF/lib/ 전체를 단일 클래스패스로 로딩
        ├── jaxws-rt-2.3.1.jar
        ├── javax.annotation-api-1.3.2.jar
        ├── javax.jws-api-1.1.jar
        └── ... (MANIFEST Class-Path 무시)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실행 방식별 비교&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;gradle bootRun&lt;/th&gt;
&lt;th&gt;java -jar&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JAR 파일 위치&lt;/td&gt;
&lt;td&gt;Maven 저장소 (분산된 디렉토리)&lt;/td&gt;
&lt;td&gt;BOOT-INF/lib (동일 계층)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ClassLoader&lt;/td&gt;
&lt;td&gt;표준 Application ClassLoader&lt;/td&gt;
&lt;td&gt;LaunchedURLClassLoader&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MANIFEST Class-Path&lt;/td&gt;
&lt;td&gt;해석하고 상대경로로 탐색&lt;/td&gt;
&lt;td&gt;무시됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;J2EE 라이브러리 호환성&lt;/td&gt;
&lt;td&gt;경고 발생 가능&lt;/td&gt;
&lt;td&gt;문제없음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;대응 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경고는 WARN 레벨이라 애플리케이션 실행 자체에는 영향을 주지 않는다. 실제로 필요한 클래스들은 Maven 의존성으로 이미 클래스패스에 포함되어 있기 때문이다. 하지만 상황에 따라 다양한 대응 방법을 선택할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전략 1: 무시하기 (권장)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;적용 상황&lt;/b&gt;: 개발 환경에서만 발생하고, 운영 환경에서는 &lt;code&gt;java -jar&lt;/code&gt;로 실행하는 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 환경에서 &lt;code&gt;bootRun&lt;/code&gt;으로 실행할 때만 발생하는 경고이므로, 원인을 이해했다면 그냥 두어도 된다. 운영 환경에서는 &lt;code&gt;bootJar&lt;/code&gt;로 패키징한 결과물을 사용하므로 문제없다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# 개발 환경 - 경고 발생하지만 동작에 문제없음
./gradlew bootRun

# 운영 환경 - 경고 없음
./gradlew bootJar
java -jar build/libs/application.jar&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전략 2: 로그 레벨 조정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;적용 상황&lt;/b&gt;: 경고 로그가 너무 많아서 다른 로그를 보기 어려운 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;application.yml&lt;/code&gt; 또는 &lt;code&gt;logback-spring.xml&lt;/code&gt;에서 해당 로거의 레벨을 ERROR로 올린다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;# application.yml
logging:
  level:
    org.apache.tomcat.util.scan.StandardJarScanner: ERROR&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!-- logback-spring.xml --&amp;gt;
&amp;lt;logger name=&quot;org.apache.tomcat.util.scan.StandardJarScanner&quot; level=&quot;ERROR&quot;/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의&lt;/b&gt;: 이 방법은 경고를 숨기는 것이지 근본적인 해결은 아니다. 다른 중요한 스캔 관련 경고도 함께 숨겨질 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전략 3: Tomcat JAR 스캔 제외 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;적용 상황&lt;/b&gt;: 특정 JAR 패턴을 스캔 대상에서 완전히 제외하고 싶은 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;application.properties&lt;/code&gt;에서 Tomcat의 JAR 스캔 제외 패턴을 설정한다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# application.properties
server.tomcat.additional-tld-skip-patterns=jaxws-rt*.jar&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 Java 설정으로 &lt;code&gt;TomcatServletWebServerFactory&lt;/code&gt;를 커스터마이징한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class TomcatConfig {

    @Bean
    public TomcatServletWebServerFactory tomcatFactory() {
        return new TomcatServletWebServerFactory() {
            @Override
            protected void postProcessContext(Context context) {
                ((StandardJarScanner) context.getJarScanner()).setScanManifest(false);
            }
        };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의&lt;/b&gt;: &lt;code&gt;setScanManifest(false)&lt;/code&gt;는 모든 MANIFEST.MF 스캔을 비활성화한다. TLD(Tag Library Descriptor) 자동 감지 등에 영향을 줄 수 있으므로 JSP를 사용하지 않는 프로젝트에서만 적용하는 것이 좋다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전략 4: 라이브러리 버전 업그레이드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;적용 상황&lt;/b&gt;: 근본적인 해결을 원하고, 라이브러리 버전 변경이 가능한 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;jaxws-rt&lt;/code&gt; 2.3.2 이상 버전에서는 MANIFEST.MF 구조가 개선되어 이 문제가 줄어든다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;// build.gradle
dependencies {
    implementation 'com.sun.xml.ws:jaxws-rt:2.3.7'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 버전 업그레이드 시 호환성 테스트가 필요하다. 특히 SOAP 웹서비스를 사용하는 경우 동작 변경이 있을 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전략 5: Jakarta EE로 마이그레이션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;적용 상황&lt;/b&gt;: 장기적인 관점에서 Java EE에서 Jakarta EE로 전환을 계획하는 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java EE 8 이후로 Jakarta EE로 전환되면서 패키지명과 라이브러리 구조가 변경됐다. Spring Boot 3.x로 업그레이드하면서 함께 마이그레이션하면 이런 레거시 문제들이 자연스럽게 해소된다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;// Jakarta EE 버전 사용
dependencies {
    implementation 'com.sun.xml.ws:jaxws-rt:4.0.1'  // Jakarta 기반
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의&lt;/b&gt;: 이 방법은 대규모 마이그레이션이 필요하므로 신중하게 결정해야 한다. &lt;code&gt;javax.*&lt;/code&gt; 패키지가 &lt;code&gt;jakarta.*&lt;/code&gt;로 변경되어 코드 수정이 필요하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전략 선택 가이드&lt;/h2&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;경고 발생
    │
    ├─ 운영 환경에서도 발생하는가?
    │   │
    │   ├─ Yes &amp;rarr; java -jar로 실행 방식 변경 검토
    │   │
    │   └─ No (개발 환경만) &amp;rarr; 전략 1 (무시) 권장
    │
    ├─ 로그가 너무 많아 다른 로그 확인이 어려운가?
    │   │
    │   └─ Yes &amp;rarr; 전략 2 (로그 레벨 조정)
    │
    ├─ JSP를 사용하지 않는 REST API 서버인가?
    │   │
    │   └─ Yes &amp;rarr; 전략 3 (스캔 비활성화) 가능
    │
    └─ 라이브러리 버전 변경이 가능한가?
        │
        ├─ Yes (단기) &amp;rarr; 전략 4 (버전 업그레이드)
        │
        └─ Yes (장기) &amp;rarr; 전략 5 (Jakarta EE 마이그레이션)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 Java EE 시대의 라이브러리 배포 방식과 현대적인 빌드 도구 + 임베디드 서버 환경 사이의 간극에서 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 WAS 환경에서는 MANIFEST.MF의 Class-Path가 의미 있게 동작했지만, Gradle이나 Maven으로 직접 실행하는 개발 환경에서는 가정이 맞지 않아 경고가 발생한다. 반면 Spring Boot의 Fat JAR로 패키징하면 모든 라이브러리가 동일 계층에 평탄화되고 전용 클래스로더가 MANIFEST를 무시하므로 문제가 해소된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인을 이해하고 나면 더 이상 당황할 필요가 없다. 상황에 맞는 전략을 선택해서 대응하면 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/current/reference/html/executable-jar.html&quot;&gt;Spring Boot Reference - Executable JAR Format&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tomcat.apache.org/tomcat-9.0-doc/config/jar-scanner.html&quot;&gt;Apache Tomcat - Jar Scanner&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Main_Attributes&quot;&gt;JAR File Specification - Class-Path Attribute&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Java&amp;amp;Spring/차세대를 하면서...</category>
      <author>무토(MUTO)</author>
      <guid isPermaLink="true">https://leemoono.tistory.com/37</guid>
      <comments>https://leemoono.tistory.com/37#entry37comment</comments>
      <pubDate>Mon, 8 Dec 2025 19:28:02 +0900</pubDate>
    </item>
    <item>
      <title>[알고리즘] 백준 - PC방 요금 (9080)</title>
      <link>https://leemoono.tistory.com/34</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cioycd/btrjl9TwN5D/3nsoMqKvpIWxJ58BoUFlcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cioycd/btrjl9TwN5D/3nsoMqKvpIWxJ58BoUFlcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cioycd/btrjl9TwN5D/3nsoMqKvpIWxJ58BoUFlcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcioycd%2Fbtrjl9TwN5D%2F3nsoMqKvpIWxJ58BoUFlcK%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;1시간마다 시간을 체크하는 방식으로 문제를 푸는 방식이 편리하다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;맨처음 진입조건에서 22시 ~ 3시에 pc방 이용을 시작하고 남은 시간이 300분 이상 남았으면 정액제를 이용한다.&lt;br&gt; 두번째 진입조건부터는 남은시간이 300분 이상인지만 체크하면 된다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;아니라면 일반 사용으로 계산한 후 2번 조건으로 진입하거나 남은 시간을 다 소진할 때까지 다시 반복하여 진행한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;생각없이 문제를 풀면 어려운 문제인것 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;

//https://www.acmicpc.net/problem/9080    [S1]    PC방 요금
public class Main {

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        int T = Integer.parseInt(br.readLine());

        for (int i = 0; i &amp;lt; T; i++) {
            String[] split = br.readLine().split(&amp;quot; &amp;quot;);
            int hour = Integer.parseInt(split[0].split(&amp;quot;:&amp;quot;)[0]);
            int minute = Integer.parseInt(split[0].split(&amp;quot;:&amp;quot;)[1]);
            int time = Integer.parseInt(split[1]);

            int fee = 0;
            while (time &amp;gt; 0) {
                if ((hour &amp;gt;= 22 || hour &amp;lt; 3) &amp;amp;&amp;amp; time &amp;gt; 300) {
                    while (hour != 8) {
                        hour = (hour + 1) % 24;
                        time -= 60;
                    }
                    fee += 5000;
                    time += minute;
                    minute = 0;
                } else {
                    hour = (hour + 1) % 24;
                    fee += 1000;
                    time -= 60;
                }
            }
            bw.write(fee + &amp;quot;\n&amp;quot;);
        }
        bw.flush();
        bw.close();
        br.close();
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>알고리즘</category>
      <category>Java</category>
      <category>백준</category>
      <category>알고리즘</category>
      <author>무토(MUTO)</author>
      <guid isPermaLink="true">https://leemoono.tistory.com/34</guid>
      <comments>https://leemoono.tistory.com/34#entry34comment</comments>
      <pubDate>Mon, 1 Nov 2021 00:58:18 +0900</pubDate>
    </item>
    <item>
      <title>[Java] 정규표현식 (기본)</title>
      <link>https://leemoono.tistory.com/31</link>
      <description>&lt;h1&gt;0. 언제 쓰는가?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정규식에는 다양한 기능이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 기능부터 살펴보도록 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 살펴볼 기능은 각종 코딩 테스트나 알고리즘에 자주 나오는 유형이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;해당 문자열이 내가 원하는 값들로만 이루어져 있는지 확인하고 싶을 때 사용한다.&lt;/li&gt;
&lt;li&gt;혹은 절대로 있으면 안되는 값이 포함되어 있는지 확인하고 싶을 때 사용한다.&lt;/li&gt;
&lt;li&gt;이것을 응용하면 내가 원하는 값이 포함 되어있는지 확인할 수도 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;1. 내가 원하는 값들만 들어있는지 확인한다.&lt;/h1&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class Main {

    private static boolean isOnlyNumber(String string) {
        return string.matches(&quot;^[0-9]*$&quot;);
    }

    private static boolean isOnlyCapitalAlphabet(String string) {
        return string.matches(&quot;^[A-Z]*$&quot;);
    }

    private static boolean isOnlySmallAlphabet(String string) {
        return string.matches(&quot;^[a-z]*$&quot;);
    }

    private static boolean isOnlyAlphabet(String string) {
        return string.matches(&quot;^[A-Za-z]*$&quot;);
    }

    private static boolean isOnlyKorean(String string) {
        return string.matches(&quot;^[가-힣]*$&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;2. 절대로 들어있으면 안되는 값이 있는지 확인한다.&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;[]&lt;/code&gt;안에 ^는 not이라는 표시이다. 이것을 활용하면 다음과 같이 표현할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class Main {

    private static boolean hasNoNumber(String string) {
        return string.matches(&quot;^[^0-9]*$&quot;);
    }

    private static boolean hasNoCapitalAlphabet(String string) {
        return string.matches(&quot;^[^A-Z]*$&quot;);
    }

    private static boolean hasNoSmallAlphabet(String string) {
        return string.matches(&quot;^[^a-z]*$&quot;);
    }

    private static boolean hasNoAlphabet(String string) {
        return string.matches(&quot;^[^A-Za-z]*$&quot;);
    }

    private static boolean hasNoKorean(String string) {
        return string.matches(&quot;^[^가-힣]*$&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;3. 내가 원하는 값이 포함 되어있는지 확인한다.&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 &lt;code&gt;!&lt;/code&gt; 연산자를 활용하자&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class Main {

    private static boolean hasNumber(String string) {
        return !string.matches(&quot;^[^0-9]*$&quot;);
    }

    private static boolean hasCapitalAlphabet(String string) {
        return !string.matches(&quot;^[^A-Z]*$&quot;);
    }

    private static boolean hasSmallAlphabet(String string) {
        return !string.matches(&quot;^[^a-z]*$&quot;);
    }

    private static boolean hasAlphabet(String string) {
        return !string.matches(&quot;^[^A-Za-z]*$&quot;);
    }

    private static boolean hasKorean(String string) {
        return !string.matches(&quot;^[^가-힣]*$&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Java&amp;amp;Spring/Java</category>
      <category>Java</category>
      <category>자바</category>
      <category>정규식</category>
      <author>무토(MUTO)</author>
      <guid isPermaLink="true">https://leemoono.tistory.com/31</guid>
      <comments>https://leemoono.tistory.com/31#entry31comment</comments>
      <pubDate>Sun, 21 Mar 2021 22:22:16 +0900</pubDate>
    </item>
    <item>
      <title>[Java] 람다</title>
      <link>https://leemoono.tistory.com/30</link>
      <description>&lt;h1&gt;0. 학습 목표&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;람다식 사용법&lt;/li&gt;
&lt;li&gt;함수형 인터페이스&lt;/li&gt;
&lt;li&gt;variable capture&lt;/li&gt;
&lt;li&gt;메소드, 생성자 레퍼런스&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;0-1. 왜 람다인가?&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;사과는 무게, 색의 데이터를 가진다. 그리고 여기에 다양한 사과들의 리스트가 있다고 가정해보자.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Apple.java&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public class Apple {
    private Color color;
    private int weight;

    public Apple(Color color, int weight) {
        this.color = color;
        this.weight = weight;
    }

    public Color getColor() {
        return color;
    }

    public void setColor(Color color) {
        this.color = color;
    }

    public int getWeight() {
        return weight;
    }

    public void setWeight(int weight) {
        this.weight = weight;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;Main.java&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public class Main {

    public static void main(String[] args) throws IOException {
        List&amp;lt;Apple&amp;gt; apples = new ArrayList&amp;lt;&amp;gt;(List.of(
            new Apple(Color.RED, 100),
            new Apple(Color.RED, 130),
            new Apple(Color.RED, 180),
            new Apple(Color.GREEN, 190),
            new Apple(Color.GREEN, 150),
            new Apple(Color.GREEN, 140),
            new Apple(Color.RED, 110),
            new Apple(Color.YELLOW, 120),
            new Apple(Color.YELLOW, 160),
            new Apple(Color.RED, 230)
        ));
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;해당 Apple 리스트에서 Color 가 GREEN인 사과를 걸러내고 싶다면 어떻게 해야할까? 다음과 같이 구현해야 할 것이다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;private static List&amp;lt;Apple&amp;gt; filterGreenApple(List&amp;lt;Apple&amp;gt; apples) {
    List&amp;lt;Apple&amp;gt; result = new ArrayList&amp;lt;&amp;gt;();
    for (Apple apple : apples) {
        if (apple.getColor().equals(Color.GREEN)) {
            result.add(apple);
        }
    }
    return result;
}&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;해당 메소드는 녹색인 사과만 골라내고 있는데 조금 더 포괄적으로 색상을 선택해서 골라낼 수 있게 바꿔보자.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;private static List&amp;lt;Apple&amp;gt; filterAppleByColor(List&amp;lt;Apple&amp;gt; apples, Color color) {
    List&amp;lt;Apple&amp;gt; result = new ArrayList&amp;lt;&amp;gt;();
    for (Apple apple : apples) {
        if (apple.getColor().equals(color)) {
            result.add(apple);
        }
    }
    return result;
}&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;색상에 추가적으로 무게로도 분류를 진행하고싶다. 한번 구현을 해보자.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;private static List&amp;lt;Apple&amp;gt; filterAppleByColor(List&amp;lt;Apple&amp;gt; apples, Color color, int weight, boolean flag) {
    List&amp;lt;Apple&amp;gt; result = new ArrayList&amp;lt;&amp;gt;();
    for (Apple apple : apples) {
        if ((flag &amp;amp;&amp;amp; apple.getColor().equals(color)) || (!flag &amp;amp;&amp;amp; apple.getWeight() &amp;gt; weight)) {
            result.add(apple);
        }
    }
    return result;
}&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;무엇을 하고싶은 코드인지 의도가 전혀 드러나지 않는다. 추가적으로 요구사항이 늘어난다면 이것을 어떻게 처리해야할지 감도 잡히지 않는다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;그렇다면 지금까지 값을 파라미터로 전달한것과는 다르게 동작을 파라미터로 전달을 해보자.&lt;/p&gt;
&lt;p&gt;선택 조건을 결정하는 인터페이스를 파라미터로 넘기는 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Main {

    public static void main(String[] args) throws IOException {
        List&amp;lt;Apple&amp;gt; apples = new ArrayList&amp;lt;&amp;gt;(List.of(
            new Apple(Color.RED, 100),
            new Apple(Color.RED, 130),
            new Apple(Color.RED, 180),
            new Apple(Color.GREEN, 190),
            new Apple(Color.GREEN, 150),
            new Apple(Color.GREEN, 140),
            new Apple(Color.RED, 110),
            new Apple(Color.YELLOW, 120),
            new Apple(Color.YELLOW, 160),
            new Apple(Color.RED, 230)
        ));

        List&amp;lt;Apple&amp;gt; greenApples = filterApples(apples,new AppleGreenColorPredicate());

    }

    private static List&amp;lt;Apple&amp;gt; filterApples(List&amp;lt;Apple&amp;gt; apples, ApplePredicate predicate) {
        List&amp;lt;Apple&amp;gt; result = new ArrayList&amp;lt;&amp;gt;();
        for (Apple apple : apples) {
            if (predicate.test(apple)) {
                result.add(apple);
            }
        }
        return result;
    }
}

interface ApplePredicate {
    boolean test(Apple apple);
}

class AppleGreenColorPredicate implements ApplePredicate{

    @Override
    public boolean test(Apple apple) {
        return apple.getColor().equals(Color.GREEN);
    }
}

class AppleHeavyWeightPredicate implements ApplePredicate{

    @Override
    public boolean test(Apple apple) {
        return apple.getWeight() &amp;gt; 100;
    }
}
&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;이제 우리가 전달한 ApplePredicate 객체에 의해서 filterApples의 동작이 결정된다. 추가적으로 필터링하고 싶으면 얼마든지 현재까지의 코드에 영향을 주지 않고 추가할 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;그러나 코드가 유연해진것은 좋으나, 만약 이 메소드를 한 번 씩 밖에 사용하지 않는 경우에는 굳이 클래스를 만들어서 이렇게 까지 해야하나 싶은 느낌이 있을것이다.&lt;/p&gt;
&lt;p&gt;그래서 자바에서는 다음과 같은 방식을 사용하여 메소드의 동작을 파라미터화 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Main {

    public static void main(String[] args) throws IOException {
        List&amp;lt;Apple&amp;gt; apples = new ArrayList&amp;lt;&amp;gt;(List.of(
            new Apple(Color.RED, 100),
            new Apple(Color.RED, 130),
            new Apple(Color.RED, 180),
            new Apple(Color.GREEN, 190),
            new Apple(Color.GREEN, 150),
            new Apple(Color.GREEN, 140),
            new Apple(Color.RED, 110),
            new Apple(Color.YELLOW, 120),
            new Apple(Color.YELLOW, 160),
            new Apple(Color.RED, 230)
        ));

        List&amp;lt;Apple&amp;gt; greenApples = filterApples(apples, new ApplePredicate() {
            @Override
            public boolean test(Apple apple) {
                return apple.getColor().equals(Color.GREEN);
            }
        });

    }

    private static List&amp;lt;Apple&amp;gt; filterApples(List&amp;lt;Apple&amp;gt; apples, ApplePredicate predicate) {
        List&amp;lt;Apple&amp;gt; result = new ArrayList&amp;lt;&amp;gt;();
        for (Apple apple : apples) {
            if (predicate.test(apple)) {
                result.add(apple);
            }
        }
        return result;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;다음과 같은 방식을 사용하면 한번만 사용 할 predicate이라면 굳이 클래스를 하나 추가적으로 만들어서 구현을 해줄 필요가 없다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;그러나 파라미터가 너무 길어져서 알아보기 힘들다는 단점이 있다.&lt;/p&gt;
&lt;p&gt;그럴때 람다식을 활용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;Apple&amp;gt; greenApples = filterApples(
    apples,
    apple -&amp;gt; apple.getColor().equals(Color.GREEN)
);&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;맨 위의 코드와 비교해서 굉장히 간단하고 편리한 코드가 되었다. 다음과 같이 람다를 사용하면 훨씬 짧은 양의 코드를 통해서 동작 파라미터화를 진행할 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;1. 람다식 사용법&lt;/h1&gt;
&lt;p&gt;람다식은 다음과 같은 방법으로 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
List&amp;lt;Apple&amp;gt; greenApples = filterApples(
    apples,
    apple -&amp;gt; apple.getColor().equals(Color.GREEN)
);

List&amp;lt;Apple&amp;gt; greenApples = filterApples(
    apples,
    apple -&amp;gt; {
        return apple.getColor().equals(Color.GREEN);
    }
);

List&amp;lt;Apple&amp;gt; greenApples = filterApples(
    apples,
    apple -&amp;gt; {
        if (apple.getColor().equals(Color.GREEN)) {
            return true;
        }
        return false;
    }
);&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;그러나 솔직히 말하면 나는 자바의 각각의 메소드에 어떤 파라미터가 있고 몇번째 파라미터가 어떤 값인지 잘 모른다.&lt;/p&gt;
&lt;p&gt;그래서 그냥 해당 인터페이스를 불러오고 IDE의 람다 표현식 변환을 사용하여 람다로 변환한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uCvSi/btqZkqzw1QS/J1RybLhmKxVwcPkg077juk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uCvSi/btqZkqzw1QS/J1RybLhmKxVwcPkg077juk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uCvSi/btqZkqzw1QS/J1RybLhmKxVwcPkg077juk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuCvSi%2FbtqZkqzw1QS%2FJ1RybLhmKxVwcPkg077juk%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;또, 여기서 우리 서터디의 자랑인 바이트코드를 살펴보지 않을 수 없다.&lt;/p&gt;
&lt;p&gt;자세히 살펴보니 밑줄 친 빨간 부분의 차이가 있었다. 위의 코드는 익명 내부 클래스를 사용하여 동작 파라미터화를 진행한 코드이고,&lt;/p&gt;
&lt;p&gt;아래의 코드는 람다를 활용하여 동작 파라미터화를 진행한 코드이다. 저 두가지의 차이점은 무엇일까?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdZEFu/btqZoyKBMU6/ags7mGLmvfPzVykneDrNKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdZEFu/btqZoyKBMU6/ags7mGLmvfPzVykneDrNKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdZEFu/btqZoyKBMU6/ags7mGLmvfPzVykneDrNKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdZEFu%2FbtqZoyKBMU6%2Fags7mGLmvfPzVykneDrNKk%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://tourspace.tistory.com/12?category=788398&quot;&gt;https://tourspace.tistory.com/12?category=788398&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;자세한 내용은 위 블로그 주소에 잘 담겨있으니 확인을 해보도록 하자.&lt;/p&gt;
&lt;p&gt;요약을 하자면, 컴파일 타임에는 어떤 방법으로 객체를 생성할지 정하지 않고 런타임에 어떤 방법으로 객체를 생성할 지 정하는 것이다.&lt;/p&gt;
&lt;p&gt;동시에 싱글턴 패턴과 비슷하게 하나의 함수형 인터페이스만을 생성하고 계속해서 재사용하는 성질도 가지고 있다.&lt;/p&gt;
&lt;p&gt;이를통해 약간의 성능상의 이점과, 추후 업데이트 될 최적화 하는 로직을 분리하여 관리할 수 있다는 장점이 있다.&lt;/p&gt;
&lt;h1&gt;2. 함수형 인터페이스&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;함수형 인터페이스는 하나의 추상메서드를 지정하는 인터페이스이다.&lt;br&gt;위에 이미 사용했던 ApplePredicate가 바로 함수형 인터페이스라고 말할 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 함수형 인터페이스로 무엇을 할 수 있을까?&lt;/p&gt;
&lt;p&gt;람다 표현식으로 함수형 인터페이스의 추상메서드 구현을 직접 전달할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스로 취급할 수 있다.&lt;/p&gt;
&lt;p&gt;왜 그러면 함수형 인터페이스를 인수로 받는 메서드에만 람다 표현식을 사용할 수 있을까? 라는 의문이 생길 수도 있는데 언어 설계자들이 언어를 더 복잡하게 만들지 않는 방향으로 개발을 진행하여 현재 하나의 추상메서드를 갖는 인터페이스만 람다에 사용할 수 있도록 설계되었다.&lt;/p&gt;
&lt;h2&gt;! @FunctionalInterface&lt;/h2&gt;
&lt;p&gt;해당 애노테이션을 선언하면 해당 인터페이스가 함수형 인터페이스가 아니면 컴파일 에러를 발생시킨다. 우리의 오류를 줄여줄 수 있는 고마운 애노테이션이다. 함수형 인터페이스를 사용할 때에는 반드시 이 애노테이션을 추가하여 넣도록 하자.&lt;/p&gt;
&lt;h1&gt;3. Variable Capture&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;람다 표현식 외부의 변수를 람다 표현식 내부에서 사용할 수 있도록 하는 동작이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;코드로 이해를 해보자&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int weight = 150;
List&amp;lt;Apple&amp;gt; HeavyApples = filterApples(
    apples,
    apple -&amp;gt; apple.getWeight() &amp;gt; weight  //컴파일 에러
);
weight = 40;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;다음과 같이 람다 외부에 있는 weight라는 변수를 람다 안에서 사용하려고 한다면 명시적으로 final을 선언해주어야한다.&lt;/p&gt;
&lt;p&gt;아니면 사실상 코드상에서 final과 같이 아무런 외부의 요인이 없어야 한다.&lt;/p&gt;
&lt;p&gt;왜 이렇게 코드를 작성 해야 하는 것일까?&lt;/p&gt;
&lt;p&gt;바로 지역변수는 jvm의 스택메모리에 값이 올라가기 때문이다. 그렇게되면 해당 값을 직접 접근하지 않고 자신의 스택에 복사한다.&lt;/p&gt;
&lt;p&gt;람다는 별도의 쓰레드에서 실행이 가능하므로 외부요인이 값을 바꾸면 immutable하지 않게 되기 때문에 final을 명시적으로 선언해주거나,&lt;/p&gt;
&lt;p&gt;혹은 사실상 새로운 병수의 할당이 없어야 하는것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int weight = 150;
List&amp;lt;Apple&amp;gt; HeavyApples = filterApples(
    apples,
    apple -&amp;gt; apple.getWeight() &amp;gt; weight  //정상적인 코드
);&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;final&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final int weight = 150;
List&amp;lt;Apple&amp;gt; HeavyApples = filterApples(
    apples,
    apple -&amp;gt; apple.getWeight() &amp;gt; weight  //정상적인 코드
);&lt;/code&gt;&lt;/pre&gt;&lt;h1&gt;4. 메소드 참조, 생성자 참조&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;기존의 메소드 정의를 재활용해서 람다처럼 전달할 수 있는 문법이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;일단 코드부터 보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLjj8f/btqZiVgd3G9/CWEfcoWgHPZXbhMdxw6wk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLjj8f/btqZiVgd3G9/CWEfcoWgHPZXbhMdxw6wk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLjj8f/btqZiVgd3G9/CWEfcoWgHPZXbhMdxw6wk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLjj8f%2FbtqZiVgd3G9%2FCWEfcoWgHPZXbhMdxw6wk1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crCPyG/btqZiVgd31H/OF3C0dgQoLmmi0n3Lzv4z0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crCPyG/btqZiVgd31H/OF3C0dgQoLmmi0n3Lzv4z0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crCPyG/btqZiVgd31H/OF3C0dgQoLmmi0n3Lzv4z0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcrCPyG%2FbtqZiVgd31H%2FOF3C0dgQoLmmi0n3Lzv4z0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUq1ZJ/btqZpUzRgEj/uXGie8BvRJxe24KUk7QjYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUq1ZJ/btqZpUzRgEj/uXGie8BvRJxe24KUk7QjYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUq1ZJ/btqZpUzRgEj/uXGie8BvRJxe24KUk7QjYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUq1ZJ%2FbtqZpUzRgEj%2FuXGie8BvRJxe24KUk7QjYk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;내가 일반적으로 메소드 참조를 사용할때 사용하는 방법이다.&lt;/p&gt;
&lt;p&gt;IDE의 도움을 받으면 메소드 참조, 람다로의 전환이 아주 편리하다.&lt;/p&gt;
&lt;p&gt;이러한 코드의 방식이 왜 중요할까? 직관적으로 판단해보자&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;가독성이 너무 좋아진다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;그렇다면 메소드 참조는 어떻게 만들까?&lt;/h3&gt;
&lt;p&gt;다음과 같은 3가지의 방법이 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;정적 메소드 참조.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;
String[] strings = new String[]{&amp;quot;1&amp;quot;,&amp;quot;2&amp;quot;,&amp;quot;3&amp;quot;};
int[] ints = Arrays.stream(strings).map(Integer::parseInt).mapToInt(v -&amp;gt; v).toArray();
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;다양한 형식의 인스턴스 메소드 참조&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;
String[] strings = new String[]{&amp;quot;1&amp;quot;,&amp;quot;2&amp;quot;,&amp;quot;3&amp;quot;};
int[] ints = Arrays.stream(strings).map(String::length).mapToInt(v-&amp;gt;v).toArray();
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;기존 객체의 인스턴스 메소드 참조.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;
apples.stream().map(Apple::getColor).toArray();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;그렇다면 생성자 참조는 무엇일까?&lt;/h3&gt;
&lt;p&gt;이것도 코드부터 보도록 하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Integer[] integers = apples.stream().map(Apple::getWeight).toArray(Integer[]::new);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;맨 뒤에 있는 new를 사용한 부분이 바로 생성자 참조이다.&lt;/p&gt;
&lt;p&gt;클래스의 이름과 ::new 키워드를 사용해서 기존 생성자의 참조를 만들 수 있다.&lt;/p&gt;
&lt;p&gt;인스턴스화 하지 않고도 생성자에 접근할 수 있는 기능이다.&lt;/p&gt;</description>
      <category>Java&amp;amp;Spring/STUDY HALLE</category>
      <author>무토(MUTO)</author>
      <guid isPermaLink="true">https://leemoono.tistory.com/30</guid>
      <comments>https://leemoono.tistory.com/30#entry30comment</comments>
      <pubDate>Fri, 5 Mar 2021 23:11:54 +0900</pubDate>
    </item>
    <item>
      <title>[실패록] EC2 카프카 메모리 리소스 부족</title>
      <link>https://leemoono.tistory.com/29</link>
      <description>&lt;h1&gt;0. 사건의 발단&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q9Puj/btqXDeBC9N3/0YgaCTFSNtTeACuZkRmid1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q9Puj/btqXDeBC9N3/0YgaCTFSNtTeACuZkRmid1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q9Puj/btqXDeBC9N3/0YgaCTFSNtTeACuZkRmid1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq9Puj%2FbtqXDeBC9N3%2F0YgaCTFSNtTeACuZkRmid1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;평화롭게 개발을 진행하던 도중 이슈가 발생했다. 갑자기 서버가 먹통이 되었다는 소식이었다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;문제를 해결하기 위해 빠르게 ec2 인스턴스를 확인해 보았으나 cpu 사용량 및 인스턴스에는 문제가 없었다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;다음은 putty로 ec2 터미널에 접속하여 top 명령어를 사용해 서버 메모리 사용량을 확인해 보았다&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;결과는 놀라웠다...&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Xxi5p/btqXLez52jx/VNI8NXswDPHfF6FyKfSagk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Xxi5p/btqXLez52jx/VNI8NXswDPHfF6FyKfSagk/img.png&quot; data-alt=&quot;분명히 시작할 때는 380m 메모리 리소스를 차지하고 있었지만 어느샌가 1.0g 0.9g 를 차지하고 있는 카프카 서버&amp;amp;amp;nbsp;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Xxi5p/btqXLez52jx/VNI8NXswDPHfF6FyKfSagk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXxi5p%2FbtqXLez52jx%2FVNI8NXswDPHfF6FyKfSagk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;분명히 시작할 때는 380m 메모리 리소스를 차지하고 있었지만 어느샌가 1.0g 0.9g 를 차지하고 있는 카프카 서버&amp;nbsp;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;내가만든 도커 이미지가 아니라 confluent가 제공하는 kafka 도커 이미지를 사용하는지라 어디서 메모리 누수가 발생하는지 파악을 할 길이 없다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;내가 할 수 있는 옵션은 무엇이 있을까?&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;도커 실행시 jvm heap opts를 256-256으로 설정한다.&lt;/li&gt;
&lt;li&gt;도커이미지를 confluent에서 만든 이미지가 아닌 다른 이미지로 변경한다.&lt;/li&gt;
&lt;li&gt;일단 당장 돌아가도록 메모리 swap을 설정하여 메모리 여유분을 만들어둔다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;우선 당장 할 수 있는 일인 1번부터 진행해보자&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kafka-dev:
    image: confluentinc/cp-kafka:5.5.1
    hostname: kafka-dev
    ports:
      - &amp;quot;9093:9093&amp;quot;
    environment:
      KAFKA_ADVERTISED_LISTENERS: LISTENER_DOCKER_INTERNAL://kafka-dev:19093,LISTENER_DOCKER_EXTERNAL://{my-ip}:9093
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: LISTENER_DOCKER_INTERNAL:PLAINTEXT,LISTENER_DOCKER_EXTERNAL:PLAINTEXT
      KAFKA_INTER_BROKER_LISTENER_NAME: LISTENER_DOCKER_INTERNAL
      KAFKA_ZOOKEEPER_CONNECT: &amp;quot;zoo-prod:2181,zoo-dev:2182,zoo-test:2183&amp;quot;
      KAFKA_BROKER_ID: 2
      KAFKA_LOG4J_LOGGERS: &amp;quot;kafka.controller=INFO,kafka.producer.async.DefaultEventHandler=INFO,state.change.logger=INFO&amp;quot;
      KAFKA_HEAP_OPTS: &amp;quot;-Xmx256m -Xms256m&amp;quot;   //옵션 추가
    volumes:
      - ./zk-multiple-kafka-multiple/kafka-dev/data:/var/lib/kafka/data
    depends_on:
      - zoo-dev&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BmKnz/btqXKv3aFkR/ExVVzQ5M6Q5YSDR4Y6K84k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BmKnz/btqXKv3aFkR/ExVVzQ5M6Q5YSDR4Y6K84k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BmKnz/btqXKv3aFkR/ExVVzQ5M6Q5YSDR4Y6K84k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBmKnz%2FbtqXKv3aFkR%2FExVVzQ5M6Q5YSDR4Y6K84k%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;우선은 해당 옵션을 사용해서 문제를 일단락 시켰고 하루정도 시간이 지나 메모리 리소스 사용량을 확인하고 여전히 메모리 누수가 발생하는지 확인해보아야 한다. 제발 해당 옵션으로 문제가 해결되길 기도한다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>실패록/오늘의 실패록</category>
      <category>heap</category>
      <category>KAFKA</category>
      <category>리눅스</category>
      <category>메모리</category>
      <author>무토(MUTO)</author>
      <guid isPermaLink="true">https://leemoono.tistory.com/29</guid>
      <comments>https://leemoono.tistory.com/29#entry29comment</comments>
      <pubDate>Thu, 18 Feb 2021 11:45:43 +0900</pubDate>
    </item>
    <item>
      <title>[실패록] Spring Controller 415 Error</title>
      <link>https://leemoono.tistory.com/28</link>
      <description>&lt;h1&gt;Controller에 HTTP문서로 요청할 때, body에 넣을 값이 없다면, 해당 content-type의 기본값을 넣어주자. 해당 body가 null이면 안된다.&lt;/h1&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
@RefreshScope
@RequestMapping(path = &quot;/sns/post&quot;, consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
@RequiredArgsConstructor
@Slf4j
public class PostController {
    .
    .
    .

    /*
        즐겨찾기 게시판 불러오기
     */
    @GetMapping(path = &quot;/favorite&quot;)
    public ResponseEntity&amp;lt;ResponseResource&amp;gt; getFavoritePosts(
            HttpServletRequest request,
            @RequestParam(&quot;lastId&quot;) int lastId,
            @RequestParam(&quot;limit&quot;) int limit
    ) {
        Long memberId = util.getTokenMemberId(request);

        Response response = Response.builder()
                .code(10)
                .message(&quot;SUCCESS&quot;)
                .body(postService.getFavoritePosts((long) lastId, limit, memberId))
                .build();

        ResponseResource resource = new ResponseResource(response, PostController.class);
        resource.add(linkTo(PostController.class).slash(&quot;favorite&quot;).withRel(&quot;get-favorite-posts&quot;));

        return ResponseEntity.ok().body(resource);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@RequestMapping(path = &quot;/sns/post&quot;, consumes = MediaType.APPLICATION_JSON_VALUE)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;프론트에서 해당 API를 요청할 때 body에 빈값을 넣었는데 415 unsupported media type 상태코드가 반환된다고 확인 부탁해달라는 요청이 들어왔다.&lt;/li&gt;
&lt;li&gt;처음에는 나도 왜 문제가 생겼는지 전혀 알 수가 없었다. 테스트코드는 무사히 통과가 되었기 때문이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;@Test @Order(11)
    @DisplayName(&quot;즐겨찾기 게시판 목록 읽기 성공&quot;)
    void getFavoritePosts() throws Exception {
        //given
        Long lastId = 0L;
        int limit = 10;

        //when
        MockHttpServletRequestBuilder builder = get(&quot;/sns/post/favorite&quot;,lastId)
                .contentType(MediaType.APPLICATION_JSON)
                .header(&quot;Authorization&quot;, &quot;Bearer &quot; + token)
                .accept(MediaTypes.HAL_JSON_VALUE)
                .param(&quot;lastId&quot;, String.valueOf(lastId))
                .param(&quot;limit&quot;, String.valueOf(limit));

        //then
        mockMvc.perform(builder)
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_VALUE + &quot;;charset=UTF-8&quot;))
                .andExpect(jsonPath(&quot;code&quot;).exists())
                .andExpect(jsonPath(&quot;message&quot;).exists())
                .andExpect(jsonPath(&quot;body&quot;,hasSize(7)))
                .andExpect(jsonPath(&quot;_links.self&quot;).exists())
                .andExpect(jsonPath(&quot;_links.get-favorite-posts&quot;).exists())
                .andDo(document(&quot;get-favorite-posts&quot;,
                        links(
                                linkWithRel(&quot;self&quot;).description(&quot;link to self&quot;),
                                linkWithRel(&quot;get-favorite-posts&quot;).description(&quot;link to favorite posts&quot;)
                        ),
                        requestHeaders(
                                headerWithName(HttpHeaders.ACCEPT).description(&quot;accept header&quot;),
                                headerWithName(HttpHeaders.CONTENT_TYPE).description(&quot;contentType header&quot;)
                        ),
                        responseHeaders(
                                headerWithName(HttpHeaders.CONTENT_TYPE).description(&quot;contentType header&quot;)
                        ),
                        responseFields(
                                fieldWithPath(&quot;code&quot;).description(&quot;label code number&quot;),
                                fieldWithPath(&quot;message&quot;).description(&quot;message&quot;),
                                fieldWithPath(&quot;body&quot;).description(&quot;body of the response&quot;),
                                fieldWithPath(&quot;body[].id&quot;).description(&quot;id&quot;),
                                fieldWithPath(&quot;body[].title&quot;).description(&quot;title&quot;),
                                fieldWithPath(&quot;body[].content&quot;).description(&quot;content&quot;),
                                fieldWithPath(&quot;body[].createdAt&quot;).description(&quot;createdAt&quot;),
                                fieldWithPath(&quot;body[].updatedAt&quot;).description(&quot;updatedAt&quot;),
                                fieldWithPath(&quot;body[].user&quot;).description(&quot;user&quot;),
                                fieldWithPath(&quot;body[].user.id&quot;).description(&quot;user id&quot;),
                                fieldWithPath(&quot;body[].user.nickname&quot;).description(&quot;user nickname&quot;),
                                fieldWithPath(&quot;body[].user.image&quot;).description(&quot;user image&quot;),
                                fieldWithPath(&quot;body[].images&quot;).description(&quot;images&quot;).optional(),
                                fieldWithPath(&quot;body[].images[].src&quot;).description(&quot;image path&quot;).optional(),
                                fieldWithPath(&quot;body[].likers&quot;).description(&quot;likers&quot;).optional(),
                                fieldWithPath(&quot;body[].likers[].id&quot;).description(&quot;id of likers&quot;).optional(),
                                fieldWithPath(&quot;body[].likers[].nickname&quot;).description(&quot;nickname of likers&quot;).optional(),
                                fieldWithPath(&quot;body[].likers[].image&quot;).description(&quot;profile image of likers&quot;).optional(),
                                fieldWithPath(&quot;body[].likers[].image.src&quot;).type(JsonFieldType.STRING).description(&quot;profile image path of likers&quot;).optional(),
                                fieldWithPath(&quot;body[].commentNum&quot;).description(&quot;commentNum&quot;),
                                fieldWithPath(&quot;body[].retweet&quot;).type(JsonFieldType.OBJECT).description(&quot;post that sharing other post&quot;).optional(),
                                fieldWithPath(&quot;body[].retweet.id&quot;).description(&quot;id of retweet&quot;),
                                fieldWithPath(&quot;body[].retweet.title&quot;).description(&quot;title of retweet&quot;),
                                fieldWithPath(&quot;body[].retweet.content&quot;).description(&quot;content of retweet&quot;),
                                fieldWithPath(&quot;body[].retweet.createdAt&quot;).description(&quot;created time of retweet&quot;),
                                fieldWithPath(&quot;body[].retweet.updatedAt&quot;).description(&quot;updated time of retweet&quot;),
                                fieldWithPath(&quot;body[].retweet.user&quot;).description(&quot;user who retweeted&quot;),
                                fieldWithPath(&quot;body[].retweet.user.id&quot;).description(&quot;user id who retweeted&quot;),
                                fieldWithPath(&quot;body[].retweet.user.nickname&quot;).description(&quot;user nickname who retweeted&quot;),
                                fieldWithPath(&quot;body[].retweet.user.image&quot;).description(&quot;user profile image who retweeted&quot;),
                                fieldWithPath(&quot;body[].retweet.images&quot;).description(&quot;images of retweet&quot;),
                                fieldWithPath(&quot;body[].retweet.images[].src&quot;).description(&quot;images path of retweet&quot;),
                                fieldWithPath(&quot;body[].retweet.likers&quot;).description(&quot;user who liked retweet&quot;),
                                fieldWithPath(&quot;body[].retweet.commentNum&quot;).description(&quot;commentNum of retweet&quot;),
                                fieldWithPath(&quot;body[].retweet.category&quot;).description(&quot;category of retweeted post&quot;),
                                fieldWithPath(&quot;body[].retweet.hits&quot;).description(&quot;views of retweet&quot;),
                                fieldWithPath(&quot;body[].retweet.blocked&quot;).description(&quot;blocking flag of retweet&quot;),
                                fieldWithPath(&quot;body[].retweet.deleted&quot;).description(&quot;whether to delete a retweet&quot;),
                                fieldWithPath(&quot;body[].category&quot;).description(&quot;category&quot;),
                                fieldWithPath(&quot;body[].hits&quot;).description(&quot;views&quot;),
                                fieldWithPath(&quot;body[].blocked&quot;).description(&quot;blocking flag of the post&quot;),
                                fieldWithPath(&quot;_links.self.href&quot;).description(&quot;link to self&quot;),
                                fieldWithPath(&quot;_links.get-favorite-posts.href&quot;).description(&quot;link to favorite posts&quot;)
                        )
                ));


    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;무엇이 문제였을까?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;컨트롤러의 consumes = MediaType.APPLICATION_JSON_VALUE 부분을 살펴보자&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;위의 방식대로 정의하면 body는 json타입으로만 읽기가 가능하다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;그리고json의 기본값은 {} 이다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;그래서 body에 {}를 집어넣고 테스트를 돌려보았다. 바로 성공했다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;왜 나는 이 문제를 테스트코드로부터 발견할 수 없었을까?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MockMvc는 content-type을 json으로 설정하면 기본 값을 {}로 테스트하고 있었기 때문이다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;그런데 혹시나 궁금해서 빈 문자열을 body 에 넣어보았는데 테스트가 성공했다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;//when
        MockHttpServletRequestBuilder builder = get(&quot;/sns/post/favorite&quot;,lastId)
                .contentType(MediaType.APPLICATION_JSON)
                .header(&quot;Authorization&quot;, &quot;Bearer &quot; + token)
                .accept(MediaTypes.HAL_JSON_VALUE)
                .content(&quot;&quot;)
                .param(&quot;lastId&quot;, String.valueOf(lastId))
                .param(&quot;limit&quot;, String.valueOf(limit));&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;다음과 같은 조건으로 content에 빈 문자열을 넣어 test를 진행해도 테스트는 무사히 통과가 되었다. 아무래도 null이 아니기만 하면 굳이{}가 아니라도 무엇이든 되는것 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /sns/post/favorite
       Parameters = {lastId=[0], limit=[10]}
          Headers = [Content-Type:&quot;application/json;charset=UTF-8&quot;, Authorization:&quot;Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJtZjAwMDEiLCJyb2xlIjoiUk9MRV9NQU5BR0VSIiwiaWQiOjEsImV4cCI6MTkxMjA2NDU4NiwiaWF0IjoxNTk2NzA0NTg2fQ.VmpRq6R0NhyteAp2ToaPPbjAANcSfZTMKvrXxCd3iFBcm3gVLn9GYd6lJQ07gRIyk_U38x4t7VEpzA2qcbMAgA&quot;, Accept:&quot;application/hal+json&quot;]
             Body = 
    Session Attrs = {}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;심지어 12345를 테스트에 넣어도 통과했다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;그럼 딱히 json이 문제는 아니었다는 소리가 된다. 그러면 대체 무엇이 문제일까?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@RequestMapping 어노테이션을 이러저리 둘러보면서 근원을 찾는 도중 문제가 되는 부분을 발견했다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;AbstractMessageConverterMethodArgumentResolver.java를 살펴보자&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;    .
    .
    .

    /**
     * Create the method argument value of the expected parameter type by reading
     * from the given HttpInputMessage.
     * @param &amp;lt;T&amp;gt; the expected type of the argument value to be created
     * @param inputMessage the HTTP input message representing the current request
     * @param parameter the method parameter descriptor
     * @param targetType the target type, not necessarily the same as the method
     * parameter type, e.g. for {@code HttpEntity&amp;lt;String&amp;gt;}.
     * @return the created method argument value
     * @throws IOException if the reading from the request fails
     * @throws HttpMediaTypeNotSupportedException if no suitable message converter is found
     */
    @SuppressWarnings(&quot;unchecked&quot;)
    @Nullable
    protected &amp;lt;T&amp;gt; Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
            Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {

        MediaType contentType;
        boolean noContentType = false;
        try {
            contentType = inputMessage.getHeaders().getContentType();
        }
        catch (InvalidMediaTypeException ex) {
            throw new HttpMediaTypeNotSupportedException(ex.getMessage());
        }
        if (contentType == null) {
            noContentType = true;
            contentType = MediaType.APPLICATION_OCTET_STREAM;
        }

        Class&amp;lt;?&amp;gt; contextClass = parameter.getContainingClass();
        Class&amp;lt;T&amp;gt; targetClass = (targetType instanceof Class ? (Class&amp;lt;T&amp;gt;) targetType : null);
        if (targetClass == null) {
            ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
            targetClass = (Class&amp;lt;T&amp;gt;) resolvableType.resolve();
        }

        HttpMethod httpMethod = (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null);
        Object body = NO_VALUE;

        EmptyBodyCheckingHttpInputMessage message;
        try {
            message = new EmptyBodyCheckingHttpInputMessage(inputMessage);

            for (HttpMessageConverter&amp;lt;?&amp;gt; converter : this.messageConverters) {
                Class&amp;lt;HttpMessageConverter&amp;lt;?&amp;gt;&amp;gt; converterType = (Class&amp;lt;HttpMessageConverter&amp;lt;?&amp;gt;&amp;gt;) converter.getClass();
                GenericHttpMessageConverter&amp;lt;?&amp;gt; genericConverter =
                        (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter&amp;lt;?&amp;gt;) converter : null);
                if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
                        (targetClass != null &amp;amp;&amp;amp; converter.canRead(targetClass, contentType))) {
                    if (message.hasBody()) {
                        HttpInputMessage msgToUse =
                                getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
                        body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
                                ((HttpMessageConverter&amp;lt;T&amp;gt;) converter).read(targetClass, msgToUse));
                        body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
                    }
                    else {
                        body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
                    }
                    break;
                }
            }
        }
        catch (IOException ex) {
            throw new HttpMessageNotReadableException(&quot;I/O error while reading input message&quot;, ex, inputMessage);
        }

        if (body == NO_VALUE) {
            if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
                    (noContentType &amp;amp;&amp;amp; !message.hasBody())) {
                return null;
            }
            throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
        }

        MediaType selectedContentType = contentType;
        Object theBody = body;
        LogFormatUtils.traceDebug(logger, traceOn -&amp;gt; {
            String formatted = LogFormatUtils.formatValue(theBody, !traceOn);
            return &quot;Read \&quot;&quot; + selectedContentType + &quot;\&quot; to [&quot; + formatted + &quot;]&quot;;
        });

        return body;
    }
    .
    .
    .
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;코드를 읽어보면 바디가 NO_VALUE가 되어선 안된다. 그 후에 body가 return되어 컨트롤러의 @RequestBody 에 해당하는 부분에 매핑이 되는데 내 코드에는 body에 매핑 될 객체의 형태가 없었기 때문에 12345,&quot;&quot;를 넣더라도 에러가 딱히 발생하지 않은것이고, 그 이전에 빈값을 넣었을 때는 415에러가 발생한 것이다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;그렇다면 이러한 문제를 앞으로 해결하기 위해 어떻게 테스트코드를 변경해야할까?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;mockMVC 테스트코드에 의도를 드러내는 수밖에 없는 것 같다. requestBuilder의 content에 {}를 사용하여 제이슨의 빈 타입이라는 의도를 드러내는 것 말고는 방도가 없어보인다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;결론&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;default 값으로 설정하지 말고 의도를 드러내어 코딩하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;
        MockHttpServletRequestBuilder builder = get(&quot;/sns/post/favorite&quot;,lastId)
                .contentType(MediaType.APPLICATION_JSON)
                .header(&quot;Authorization&quot;, &quot;Bearer &quot; + token)
                .accept(MediaTypes.HAL_JSON_VALUE)
                .content(&quot;{}&quot;) //비어있는 것을 표시하자
                .param(&quot;lastId&quot;, String.valueOf(lastId))
                .param(&quot;limit&quot;, String.valueOf(limit));

        //then
        mockMvc.perform(builder)
        .
        .
        .&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PS. 테스트 코드가 조금 지저분한것같다. 중복을 제거하는 방향으로 테스트 코드를 정리할 필요가 있겠다.&lt;/h3&gt;</description>
      <category>실패록/오늘의 실패록</category>
      <category>415</category>
      <category>@RequestMapping</category>
      <category>Java</category>
      <category>spring</category>
      <category>실패록</category>
      <author>무토(MUTO)</author>
      <guid isPermaLink="true">https://leemoono.tistory.com/28</guid>
      <comments>https://leemoono.tistory.com/28#entry28comment</comments>
      <pubDate>Mon, 1 Feb 2021 01:45:49 +0900</pubDate>
    </item>
    <item>
      <title>[Java] enum</title>
      <link>https://leemoono.tistory.com/27</link>
      <description>&lt;h1&gt;0. 학습 목표&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;자바의 열거형에 대해서 학습한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;enum 정의하는 방법&lt;/li&gt;
&lt;li&gt;enum이 제공하는 메소드 (values()와 valueOf())&lt;/li&gt;
&lt;li&gt;java.lang.Enum&lt;/li&gt;
&lt;li&gt;EnumSet&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;1. enum 정의하는 방법&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;자바의 enum을 정의하는법은 일반적으로 클래스를 사용하는 방법과 다르지 않다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1-1.기본형&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package study.moon.test;

public enum Word {
    HELLO,
    WORLD

}&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;말 그대로 데이터를 열거하는 방식으로 이루어져 있다. 마치 값이 채워진 배열과 같은 느낌이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1-2.데이터 표현형&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package study.moon.test;

public enum Word {
    HELLO(&amp;quot;HELLO&amp;quot;),
    WORLD(&amp;quot;WORLD&amp;quot;);

    String word;

    Word(String word) {
        this.word = word;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;enum 내부에 멤버변수를 생성하고, 생성자를 만들게 되면 열거형에 타입을 정해주어야한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package study.moon.test;

public enum Word {
    HELLO(&amp;quot;HELLO&amp;quot;),
    WORLD(&amp;quot;WORLD&amp;quot;),

    MY_NAME(&amp;quot;MY_NAME&amp;quot;,1),
    IS_MOON(&amp;quot;IS_MOON&amp;quot;,2);

    String word;
    int number;

    Word(String word) {
        this.word = word;
    }

    Word(String word, int number) {
        this.word = word;
        this.number = number;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;다음과 같이 여러개의 생성자를 만들 수도 있으며, 여러개의 생성자를 만들 경우, 해당 타입을 골라서 채워넣을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;2. enum이 제공하는 메소드&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;한번 까보자 enum은 도대체 무엇을 하는 녀석일까?&lt;/li&gt;
&lt;li&gt;우리 스터디의 상징 바이트코드를 확인해보도록 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEn8wE/btqVddjirqa/tQF6GPD6ruaqUzAisbOYWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEn8wE/btqVddjirqa/tQF6GPD6ruaqUzAisbOYWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEn8wE/btqVddjirqa/tQF6GPD6ruaqUzAisbOYWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEn8wE%2FbtqVddjirqa%2FtQF6GPD6ruaqUzAisbOYWK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;웃기는 녀석이다. 한 아무것도 한것이 없는데 안에서 어떤 일들이 일어났는지 살펴보자.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;자기 멋대로 java.lang.Enum&amp;lt;&lt;code&gt;Word&lt;/code&gt;&amp;gt;를 상속받고있다.&lt;/li&gt;
&lt;li&gt;만들어둔 열거형은 static으로 전역이다.&lt;/li&gt;
&lt;li&gt;거기에 final로 재할당까지 금지해두었다.&lt;/li&gt;
&lt;li&gt;상속의 결과로 내 클래스 타입의 배열을 리턴하는 values()메소드,&lt;/li&gt;
&lt;li&gt;그리고 valueOf(String) 메소드를 가진다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;4번과 5번의 내용부터 먼저 살펴보자.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Main.java&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package study.moon.test;

import java.util.Arrays;

public class Main {

    public static void main(String[] args) {
        System.out.println(Word.valueOf(&amp;quot;WORLD&amp;quot;));
        System.out.println(Arrays.toString(Word.values()));
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;Word.java&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package study.moon.test;

public enum Word {
    OH,
    HELLO(&amp;quot;HELLO&amp;quot;),
    WORLD(&amp;quot;WORLD&amp;quot;),

    MY_NAME(&amp;quot;MY_NAME&amp;quot;,1),
    IS_MOON(&amp;quot;IS_MOON&amp;quot;,2);

    String word;
    int number;

    Word() {
    }

    Word(String word) {
        this.word = word;
    }

    Word(String word, int number) {
        this.word = word;
        this.number = number;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRXroq/btqVcrB8bOy/xpUlIqHAv6c8UiPcHX9hS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRXroq/btqVcrB8bOy/xpUlIqHAv6c8UiPcHX9hS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRXroq/btqVcrB8bOy/xpUlIqHAv6c8UiPcHX9hS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRXroq%2FbtqVcrB8bOy%2FxpUlIqHAv6c8UiPcHX9hS1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;어떤 역할을 하는 메소드인지 유추가 가능하다. 추상화가 잘되어있어서 직관적이다.&lt;/li&gt;
&lt;li&gt;values()는 내부의 열거형을 배열의 형태로 순서대로 return 한다. 따라서 다음과 같은 표현식도 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package study.moon.test;

public class Main {

    public static void main(String[] args) {
        System.out.println(Word.values()[0]);
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;valueOf(String name)은 enum 내부에 있는 열거중 해당 이름을 가진 것의 name필드를 가져온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;3. java.lang.Enum&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;이녀석을 한번 살펴보자. 그래야 열거형에 대해서 조금 제대로 알 수 있을것 같다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;세가지를 구현을 해야한다고 적혀있다.&lt;br&gt;구현 내용을 살펴보면&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Constantable&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;constant를 보장받고&lt;br&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ciWW6n/btqVbYz9fnE/fAAWnLPZWv1IsoheXC8Ldk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ciWW6n/btqVbYz9fnE/fAAWnLPZWv1IsoheXC8Ldk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciWW6n/btqVbYz9fnE/fAAWnLPZWv1IsoheXC8Ldk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FciWW6n%2FbtqVbYz9fnE%2FfAAWnLPZWv1IsoheXC8Ldk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Comparable&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ordinal을 기준으로 정렬을 한다.&lt;br&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lYmio/btqU4ZG3DWN/rlYTfL5Jo3IQaX3ClnWBF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lYmio/btqU4ZG3DWN/rlYTfL5Jo3IQaX3ClnWBF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lYmio/btqU4ZG3DWN/rlYTfL5Jo3IQaX3ClnWBF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlYmio%2FbtqU4ZG3DWN%2FrlYTfL5Jo3IQaX3ClnWBF1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Main.java&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package study.moon.test;

import java.util.Arrays;

public class Main {

    public static void main(String[] args) {
        System.out.println(Word.values()[0]);
        Word[] words = new Word[4];
        words[0] = Word.MY_NAME;
        words[1] = Word.HELLO;
        words[2] = Word.IS_MOON;
        words[3] = Word.WORLD;
        Arrays.sort(words);
        System.out.println(Arrays.toString(words));
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgUu04/btqU94gzqiq/Mx32KtUBI5fg6KCi8QTIA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgUu04/btqU94gzqiq/Mx32KtUBI5fg6KCi8QTIA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgUu04/btqU94gzqiq/Mx32KtUBI5fg6KCi8QTIA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgUu04%2FbtqU94gzqiq%2FMx32KtUBI5fg6KCi8QTIA1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Serializable&lt;ul&gt;
&lt;li&gt;직렬화도 적용된다.&lt;br&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjLBhE/btqU93IMXrK/4B9unq4pH88wbPc3YZXTmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjLBhE/btqU93IMXrK/4B9unq4pH88wbPc3YZXTmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjLBhE/btqU93IMXrK/4B9unq4pH88wbPc3YZXTmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjLBhE%2FbtqU93IMXrK%2F4B9unq4pH88wbPc3YZXTmk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;핵심적인 멤버변수는 다음의 2개이다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;String name&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;선언되는 해당 enum 상수의 이름이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;int ordinlal&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;name과 같은 짝을 이루어 부여되는 int형 숫자이다. 0부터 시작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아까 보았던 메소드도 한번 살펴보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYYngh/btqVbXgYsKd/olj0oq9gjk1otTNbW7iaQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYYngh/btqVbXgYsKd/olj0oq9gjk1otTNbW7iaQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYYngh/btqVbXgYsKd/olj0oq9gjk1otTNbW7iaQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYYngh%2FbtqVbXgYsKd%2Folj0oq9gjk1otTNbW7iaQK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;그런데 아까 보았던 values()는 찾을수가 없다. 대체 어디서 온 메소드인 것일까?&lt;/p&gt;
&lt;h1&gt;4. EnumSet&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dvifgC/btqVe7bPn7X/JNBFNovWb6slMksO520rp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dvifgC/btqVe7bPn7X/JNBFNovWb6slMksO520rp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dvifgC/btqVe7bPn7X/JNBFNovWb6slMksO520rp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdvifgC%2FbtqVe7bPn7X%2FJNBFNovWb6slMksO520rp0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;이펙티브 자바의 저자 조슈아 블로크씨가 enum을 사용하면 더욱 효율적인 set과 map을 구현할 수 있다고 적어놓았다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package study.moon.test;

import java.util.Arrays;
import java.util.EnumSet;

public class Main {

    public static void main(String[] args) {
        Word[] words = new Word[4];
        words[0] = Word.MY_NAME;
        words[1] = Word.HELLO;
        words[2] = Word.IS_MOON;
        words[3] = Word.WORLD;
        Arrays.sort(words);
        System.out.println(Arrays.toString(words));
        EnumSet&amp;lt;Word&amp;gt; enumSet1 = EnumSet.allOf(Word.class);
        EnumSet&amp;lt;Word&amp;gt; enumSet2 = EnumSet.of(words[0],words[1]);

        System.out.println(enumSet1);
        System.out.println(enumSet2);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;다음과 같은 사용 방법을 가지는데 솔직히 EnumSet의 장점을 딱히 느끼진 못하겠다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;5. 느낀점&lt;/h1&gt;
&lt;p&gt;1) 하나의 비슷한 타입을 열거할 때 배열 말고 enum을 사용하여 이름지어 사용하는것이 괜찮을것 같다.&lt;/p&gt;
&lt;p&gt;2) 각각의 타입이 모두 따로 정해져있고 정해져있는 값들끼리의 상호작용이 필요한 경우 고려해볼 방법인것 같다.&lt;/p&gt;</description>
      <category>Java&amp;amp;Spring/STUDY HALLE</category>
      <category>enum</category>
      <category>Java</category>
      <category>study halle</category>
      <category>whiteship</category>
      <category>열거형</category>
      <category>자바</category>
      <author>무토(MUTO)</author>
      <guid isPermaLink="true">https://leemoono.tistory.com/27</guid>
      <comments>https://leemoono.tistory.com/27#entry27comment</comments>
      <pubDate>Sat, 30 Jan 2021 03:43:39 +0900</pubDate>
    </item>
    <item>
      <title>[Java] 쓰레드 Thread</title>
      <link>https://leemoono.tistory.com/26</link>
      <description>&lt;h1&gt;0. 학습목표&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;자바의 멀티쓰레드 프로그래밍에 대해서 학습한다.&lt;br&gt;어려워지려고 하면 너무 끝도 없이 어려워지니 정말 기본적으로 쓰레드를 이해할 수 있을 정도의 수준과 예제만 담도록 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Thread 클래스&lt;/li&gt;
&lt;li&gt;Runnable 인터페이스&lt;/li&gt;
&lt;li&gt;메인쓰레드&lt;/li&gt;
&lt;li&gt;쓰레드의 상태&lt;/li&gt;
&lt;li&gt;쓰레드의 우선순위&lt;/li&gt;
&lt;li&gt;동기화&lt;/li&gt;
&lt;li&gt;데드락&lt;/li&gt;
&lt;li&gt;싱글턴에서의 Thread-safe&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;1. Thread&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;인텔리제이의 기능을 이용하여 Thread클래스 명세를 읽어보며 공부를 해보자.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lFxcu/btqT17Zylub/EeEtflzN2M99yQ3bL0gZoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lFxcu/btqT17Zylub/EeEtflzN2M99yQ3bL0gZoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lFxcu/btqT17Zylub/EeEtflzN2M99yQ3bL0gZoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlFxcu%2FbtqT17Zylub%2FEeEtflzN2M99yQ3bL0gZoK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;스레드는 하나의 프로그램에서의 실행 흐름이다.&lt;/li&gt;
&lt;li&gt;JVM은 병렬적으로 작동하는 여러개의 스레드 실행을 허용한다.&lt;/li&gt;
&lt;li&gt;모든 스레드는 우선순위가 있다. 우선순위가 높은 스레드는 우선순위가 낮은 스레드보다 먼저 실행된다.&lt;/li&gt;
&lt;li&gt;어떤 스레드는 데몬스레드가 되거나 되지 않을수 있다.&lt;/li&gt;
&lt;li&gt;일부 스레드에서 실행중인 코드가 새 스레드 객체를 생성할 때, 새 스레드는 처음에 생선된 스레드의 우선순위와 동일하게 설정된 우선순위를 가지며, 생성스레드가 데몬인 경우에만 데몬스레드가 된다.&lt;/li&gt;
&lt;li&gt;JVM이 시작될 때 일반적으로 메인메서드의 호출로 발생한 단일 비데몬 스레드가 있다.&lt;/li&gt;
&lt;li&gt;JVM은 다음과 같은 상황이 발생할 때 까지 지속된다.&lt;ul&gt;
&lt;li&gt;Runtime 클래스의 exit() 메서드가 호출되고 security manager가 종료 조작을 허가한 경우.&lt;/li&gt;
&lt;li&gt;데몬 스레드가 아닌 모든 스레드가 run()메서드의 호출로 return되었거나, run()메서드를 넘어서 전파되는 예외를 throw하여 죽은경우.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;스레드는 두 가지의 실행방식이 있다. 첫 번째는 Thread 클래스의 서브클래스로 선언되는것이다. 이 서브클래스는 반드시 Thread클래스의 run()메서드를 오버라이딩 해야한다. 그런 다음에야 서브클래스의 인스턴스를 할당하고 시작할 수 있다.&lt;/li&gt;
&lt;li&gt;그 후 인스턴스의 start()메서드를 호출하면 스레드를 실행할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k85nB/btqT6Hsq3Q5/xpf25QkNsDrsdwLkVLlpzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k85nB/btqT6Hsq3Q5/xpf25QkNsDrsdwLkVLlpzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k85nB/btqT6Hsq3Q5/xpf25QkNsDrsdwLkVLlpzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk85nB%2FbtqT6Hsq3Q5%2Fxpf25QkNsDrsdwLkVLlpzk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;또 다른 방법은 Runnable 인터페이스를 구현하는 클래스를 작성하는 것이다. 그 클래스는 run()메서드를 구현해야한다.&lt;/li&gt;
&lt;li&gt;새로운 스레드의 인수로 Runnable인스턴스를 인자로 넘긴 후, 해당 스레드를 실행하면 스레드를 실행할 수 있다.&lt;/li&gt;
&lt;li&gt;모든 스레드는 식별을 위한 이름이 있다.&lt;/li&gt;
&lt;li&gt;둘 이상의 스레드가 동일한 이름을 가질 수 있다.&lt;/li&gt;
&lt;li&gt;스레드가 생성될 때 이름이 지정되지 않으면 새 이름이 생성된다.&lt;/li&gt;
&lt;li&gt;달리 명시되지 않는 한, 이 클래스의 생성자, 또는 메서드에 null 인수를 전달하면 NullPointerException이 throw된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;설명을 읽어봤으니 비슷하게 따라 쳐보자.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;package study.moon.Test;

public class Main {

    public static void main(String[] args) {
        Data data = new Data(0);
        MyThread1 thread1 = new MyThread1(&amp;quot;thread-1&amp;quot;,data);
        MyThread1 thread2 = new MyThread1(&amp;quot;thread-2&amp;quot;,data);
        MyThread2 thread3 = new MyThread2(&amp;quot;thread-3&amp;quot;,data);
        MyThread2 thread4 = new MyThread2(&amp;quot;thread-4&amp;quot;,data);

        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();

    }
}

class Data {
    int number;

    public Data(int number) {
        this.number = number;
    }
}

class MyThread1 extends Thread{
    String name;
    Data data;
    int number;

    public MyThread1(String name, Data data) {
        this.name = name;
        this.data = data;
        this.number = 1;
    }

    public void run() {
        for (int i = 0; i &amp;lt; 5; i++) {
            data.number = number;
            number++;
            System.out.println(name+&amp;quot;:&amp;quot;+data.number);
        }
    }
}

class MyThread2 extends Thread{
    String name;
    Data data;
    int number;

    public MyThread2(String name, Data data) {
        this.name = name;
        this.data = data;
        this.number = 1;
    }

    public void run() {
        for (int i = 0; i &amp;lt; 5; i++) {
            data.number = number;
            number--;
            System.out.println(name+&amp;quot;:&amp;quot;+data.number);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;싱글스레드 환경에서는 정답이 0이 나와야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkFRGs/btqTYR4lN8c/y3BWs13XE4VtXdUM0Otar1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkFRGs/btqTYR4lN8c/y3BWs13XE4VtXdUM0Otar1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkFRGs/btqTYR4lN8c/y3BWs13XE4VtXdUM0Otar1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkFRGs%2FbtqTYR4lN8c%2Fy3BWs13XE4VtXdUM0Otar1%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;값이 출력되는것을 보아하니 스레드의 실행순서가 제멋대로이다. 날뛰는 이녀석들을 제어하기는 요원해보인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;메서드 구성&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;이번 주요 내용과 관련이 있는 일부 메서드들을 찾아보자&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;sleep()&lt;ul&gt;
&lt;li&gt;시스템 타이머 및 스케줄러의 정밀도에 따라 현재 실행중인 스레드를 지정된 밀리초동안 휴면한다. 스레드는 모니터의 소유권을 잃지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;yield()&lt;ul&gt;
&lt;li&gt;현재 스레드가 프로세서의 현재 사용을 양보할 의사가 있다는 스케줄러에 대한 힌트이다. 보통 디버깅 또는 테스트 목적에 유용하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;clone() 불가능&lt;ul&gt;
&lt;li&gt;클론이 불가능하다. 호출시 예외를 던진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;start()&lt;ul&gt;
&lt;li&gt;스레드를 실행한다. 스레드를 두번 시작할 수 없고, 스레드가 실행 완료된 후에도 다시 시작할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;run()&lt;ul&gt;
&lt;li&gt;스레드에 할당된 runnable을 실행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;exit()&lt;ul&gt;
&lt;li&gt;스레드가 실제로 종료되기 전에 정리할 기회를 주기 위해 시스템에 의해 호출된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;interrupt()&lt;ul&gt;
&lt;li&gt;이 스레드를 중단한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;join()&lt;ul&gt;
&lt;li&gt;해당 메서드가 죽을때까지 최대 파라미터ms만큼 기다린다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;set/getPriority()&lt;ul&gt;
&lt;li&gt;스레드의 우선순위 get/set한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;getState()&lt;ul&gt;
&lt;li&gt;스레드의 상태를 get/set 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;2. Runnable&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;run()메서드 하나만을 가지고있는 인터페이스이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;package study.moon.Test;

public class Main {

    public static void main(String[] args) {
        Data data = new Data(0);
        MyThread3 thread1 = new MyThread3(data,0,&amp;quot;Thread-1&amp;quot;);
        MyThread3 thread2 = new MyThread3(data,0,&amp;quot;Thread-2&amp;quot;);
        MyThread3 thread3 = new MyThread3(data,0,&amp;quot;Thread-3&amp;quot;);
        MyThread3 thread4 = new MyThread3(data,0,&amp;quot;Thread-4&amp;quot;);
        new Thread(thread1).start();
        new Thread(thread2).start();
        new Thread(thread3).start();
        new Thread(thread4).start();

    }
}

class Data {
    int number;

    public Data(int number) {
        this.number = number;
    }
}

class MyThread3 implements Runnable {

    public MyThread3(Data data, int number, String name) {
        this.data = data;
        this.number = number;
        this.name = name;
    }

    Data data;
    int number;
    String name;
    @Override
    public void run() {
        for (int i = 0; i &amp;lt; 5; i++) {
            data.number = number;
            number++;
            System.out.println(name+&amp;quot;:&amp;quot;+data.number);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;실행결과는 역시 전과 같이 제멋대로 순서없이 돌아가고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhrxIb/btqT6HMX9Zh/UwtwmHFK9HYWSrY0C9eE31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhrxIb/btqT6HMX9Zh/UwtwmHFK9HYWSrY0C9eE31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhrxIb/btqT6HMX9Zh/UwtwmHFK9HYWSrY0C9eE31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhrxIb%2FbtqT6HMX9Zh%2FUwtwmHFK9HYWSrY0C9eE31%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;3. 메인쓰레드&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;main() 메서드를 실행하는 쓰레드이다. 모든 자바 어플리케이션은 메인쓰레드가 메인메소드를 호출하면서 시작한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;4. 스레드의 상태&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;스레드는 다음과 같은 상태중 하나를 가진다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;new&lt;ul&gt;
&lt;li&gt;아직 시작하지 않은 상태이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;runnable&lt;ul&gt;
&lt;li&gt;jvm에서 실행중인 상태이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;blocked&lt;ul&gt;
&lt;li&gt;모니터락을 기다리면서 블럭된 상태이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;waiting&lt;ul&gt;
&lt;li&gt;다른 스레드가 특정 작업을 수행할 때까지 무기한 대기중인 상태.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;timed-waiting&lt;ul&gt;
&lt;li&gt;다른 스레드가 지정된 대기시간까지 작업을 수행하기를 기다리는 상태.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;terminated&lt;ul&gt;
&lt;li&gt;종료된 상태.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yqD9Y/btqTYRi30rS/NEGMaVQTyRYlDy4mtlNlV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yqD9Y/btqTYRi30rS/NEGMaVQTyRYlDy4mtlNlV1/img.png&quot; data-alt=&quot;https://www.javamadesoeasy.com/2015/03/thread-states-thread-life-cycle-in-java.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yqD9Y/btqTYRi30rS/NEGMaVQTyRYlDy4mtlNlV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyqD9Y%2FbtqTYRi30rS%2FNEGMaVQTyRYlDy4mtlNlV1%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://www.javamadesoeasy.com/2015/03/thread-states-thread-life-cycle-in-java.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;5. 스레드의 우선순위&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;스레드의 우선순위는 1이 가장 낮고 10이 가장 높다. setPriority() 메서드를 사용해서 우선순위를 지정할 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이것을 잘 사용하면 프로그램의 우선순위를 정하여 스레드를 제어할 수 있을 것 같다 한번 코드를 작성해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package study.moon.Test;

public class Main {

    public static void main(String[] args) {
        Data data = new Data(0);
        MyThread1 thread1 = new MyThread1(data,10);
        MyThread1 thread2 = new MyThread1(data,7);
        MyThread2 thread3 = new MyThread2(data,4);
        MyThread2 thread4 = new MyThread2(data,1);

        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

class Data {
    int number;

    public Data(int number) {
        this.number = number;
    }
}

class MyThread1 extends Thread{
    Data data;
    int number;

    public MyThread1(Data data, int priority) {
        this.data = data;
        this.number = 1;
        setPriority(priority);
    }

    public void run() {
        for (int i = 0; i &amp;lt; 5; i++) {
            data.number = number;
            number++;
            System.out.println(this.getName()+&amp;quot;:&amp;quot;+data.number);
        }
    }

}

class MyThread2 extends Thread{
    Data data;
    int number;

    public MyThread2(Data data, int priority) {
        this.data = data;
        this.number = 1;
        setPriority(priority);
    }

    public void run() {
        for (int i = 0; i &amp;lt; 5; i++) {
            data.number = number;
            number--;
            System.out.println(this.getName()+&amp;quot;:&amp;quot;+data.number);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b47lr7/btqT91KHYzt/kAq9PXvzT4eplY9dBTkZK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b47lr7/btqT91KHYzt/kAq9PXvzT4eplY9dBTkZK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b47lr7/btqT91KHYzt/kAq9PXvzT4eplY9dBTkZK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb47lr7%2FbtqT91KHYzt%2FkAq9PXvzT4eplY9dBTkZK1%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;하지만 여전히 제멋대로이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Why?&lt;/h3&gt;
&lt;p&gt;프로그램의 해당 내용을 들여다 보았을 때, cpu의 할당이 필요하지 않은 데이터의 입출력에 대한 비율이 높은 것을 알 수 있다. 이러한 상황에서는 쓰레드는 무리하게 cpu를 차지하려고 하지 않는다고 한다. 오히려 이런 상황에서는 자신에게 할당된 cpu를 다른 쓰레드에게 넘겨 우선순위가 낮은 쓰레드가 실행의 기회를 얻는다고 한다.&lt;/p&gt;
&lt;h1&gt;6. 동기화&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;하나의 공유자원을 여러 쓰레드가 각각의 작업을 진행한다면 필연적으로 문제가 발생한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;다음 결과를 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package study.moon.Test;

import static java.lang.Thread.sleep;

public class Main {

    public static void main(String[] args) throws InterruptedException {
        Data data = new Data(0);
        MyThread1 thread1 = new MyThread1(data,1);
        MyThread1 thread2 = new MyThread1(data,1);
        MyThread1 thread3 = new MyThread1(data,1);

        thread1.start();
        thread2.start();
        thread3.start();

        sleep(1000);
        System.out.println(data.number);//10098396

    }
}

class Data {
    int number;

    public Data(int number) {
        this.number = number;
    }

    public void plus() {
        number++;
    }
}

class MyThread1 extends Thread{
    Data data;
    int number;
    long time;

    public MyThread1(Data data, int priority) {
        this.data = data;
        this.number = 0;
        setPriority(priority);
    }

    public void run() {
        for (int i = 0; i &amp;lt; 10_000_000; i++) {
            data.plus();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;원래대로라면 10,000,000을 세번 더했으니 30,000,000 이 결괏값으로 나와야 정상이다.&lt;br&gt;하지만 전혀다른 이상한 값이 나왔다. 서로가 데이터의 값을 읽어들였을 때 이미 다른 쓰레드가 해당 데이터를 변경한 뒤이기 때문에 데이터의 일관성이 보장되지 않는 상황인 것이다.&lt;br&gt;이 상황을 해결하기 위해 자바는 synchronized 라는 키워드가 있다. 해당 키워드를 사용하면 해당 값을 변경할 때 다른 실행흐름이 해당 자원에 접근하지 못하도록 락을 건다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;package study.moon.Test;

import static java.lang.Thread.sleep;

public class Main {

    public static void main(String[] args) throws InterruptedException {
        Data data = new Data(0);
        MyThread1 thread1 = new MyThread1(data,1);
        MyThread1 thread2 = new MyThread1(data,1);
        MyThread1 thread3 = new MyThread1(data,1);

        thread1.start();
        thread2.start();
        thread3.start();

        sleep(1000);
        System.out.println(data.number);//30,000,000
    }
}

class Data {
    int number;

    public Data(int number) {
        this.number = number;
    }

    public synchronized void plus() {
        number++;
    }
}

class MyThread1 extends Thread{
    Data data;
    int number;
    long time;

    public MyThread1(Data data, int priority) {
        this.data = data;
        this.number = 0;
        setPriority(priority);
    }

    public void run() {
        for (int i = 0; i &amp;lt; 10_000_000; i++) {
            data.plus();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;쓰레드 문제로부터 안전해졌지만 성능이 안좋아졌다. 다음의 두 코드를 보고 비교해보자.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;그냥 메서드&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package study.moon.Test;

public class Main {

    public static void main(String[] args) throws InterruptedException {
        Data data = new Data(0);
        MyThread1 thread1 = new MyThread1(data,1);
        MyThread1 thread2 = new MyThread1(data,1);
        MyThread1 thread3 = new MyThread1(data,1);

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

class Data {
    int number;

    public Data(int number) {
        this.number = number;
    }

    public void plus() {
        number++;
    }
}

class MyThread1 extends Thread{
    Data data;
    int number;
    long time;

    public MyThread1(Data data, int priority) {
        this.data = data;
        this.number = 0;
        setPriority(priority);
    }

    public void run() {
        long start = System.currentTimeMillis();
        for (int i = 0; i &amp;lt; 10_000_000; i++) {
            data.plus();
        }
        long end = System.currentTimeMillis();
        time = end-start;
        System.out.println(time);
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;결괏값 : 11 11 11&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;synchronized&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package study.moon.Test;

public class Main {

    public static void main(String[] args) throws InterruptedException {
        Data data = new Data(0);
        MyThread1 thread1 = new MyThread1(data,1);
        MyThread1 thread2 = new MyThread1(data,1);
        MyThread1 thread3 = new MyThread1(data,1);

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

class Data {
    int number;

    public Data(int number) {
        this.number = number;
    }

    public void plus() {
        number++;
    }
}

class MyThread1 extends Thread{
    Data data;
    int number;
    long time;

    public MyThread1(Data data, int priority) {
        this.data = data;
        this.number = 0;
        setPriority(priority);
    }

    public void run() {
        long start = System.currentTimeMillis();
        for (int i = 0; i &amp;lt; 10_000_000; i++) {
            data.plus();
        }
        long end = System.currentTimeMillis();
        time = end-start;
        System.out.println(time);
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;결괏값 : 813 818 819&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;해당 연산의 경우 대략 80배의 성능차이가 발생한다.&lt;/p&gt;
&lt;h1&gt;7. 데드락(교착상태)&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MshYW/btqTYR4m4Pu/MokUJRRaokq4CMGyq0tBD0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MshYW/btqTYR4m4Pu/MokUJRRaokq4CMGyq0tBD0/img.gif&quot; data-alt=&quot;http://denninginstitute.com/itcore/processes/Dead.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MshYW/btqTYR4m4Pu/MokUJRRaokq4CMGyq0tBD0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/MshYW/btqTYR4m4Pu/MokUJRRaokq4CMGyq0tBD0/img.gif&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;http://denninginstitute.com/itcore/processes/Dead.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;위의 그림과 같이 두개 이상의 작업이 서로 상대방의 작업이 끝나기만을 기다리고있기 때문에 프로세스가 자원을 얻지 못해 다음 처리를 진행할 수 없는 상태를 말한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;데드락이 발생하기 위해서는 다음 네가지의 조건을 모두 만족시켜야한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;상호배제(Mutual exclusion)Permalink&lt;br&gt;자원을 한 번에 한 프로세스만이 사용하는 경우.&lt;/li&gt;
&lt;li&gt;점유대기(Hold and wait)Permalink&lt;br&gt;최소한 하나의 자원을 점유하고 있으면서 다른 프로세스에 할당되어 사용하고 있는 자원을 추가로 점유하기 위해 대기하는 프로세스가 있는 경우.&lt;/li&gt;
&lt;li&gt;비선점(No preemption)Permalink&lt;br&gt;다른 프로세스에 할당된 자원은 사용이 끝날 때까지 강제로 빼앗을 수 없는 경우.&lt;/li&gt;
&lt;li&gt;순환대기(Circular wait)Permalink&lt;br&gt;각 프로세스는 순환적으로 다음 프로세스가 요구하는 자원을 가지고 있는 경우.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;다음 조건을 확인하여 예제를 작성해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package study.moon.Test;

public class Main {

    public static void main(String[] args) {
        final Object car1 = &amp;quot;car1&amp;quot;;
        final Object car2 = &amp;quot;car2&amp;quot;;
        Thread t1 = new Thread(() -&amp;gt; {
            synchronized (car1) {
                System.out.println(&amp;quot;Thread 1: locked car 1&amp;quot;);
                synchronized (car2) {
                    System.out.println(&amp;quot;Thread 1: locked car 2&amp;quot;);
                }
            }
        });
        Thread t2 = new Thread(() -&amp;gt; {
            synchronized (car2) {
                System.out.println(&amp;quot;Thread 2: locked car 2&amp;quot;);
                synchronized (car1) {
                    System.out.println(&amp;quot;Thread 2: locked car 1&amp;quot;);
                }
            }
        });

        t1.start();
        t2.start();
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UDL9s/btqTYRJ8UPc/KeP1RLx2VkZeIs7McjKkb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UDL9s/btqTYRJ8UPc/KeP1RLx2VkZeIs7McjKkb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UDL9s/btqTYRJ8UPc/KeP1RLx2VkZeIs7McjKkb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUDL9s%2FbtqTYRJ8UPc%2FKeP1RLx2VkZeIs7McjKkb0%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;데드락이 발생했다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;다음과 같이 서비스 운영중에 데드락이 발생했을 경우 회사의 큰 손해로 이어질 수도 있기 때문에 항상 서비스가 데드락에서 빠져나올 수 있는 해결방안을 미리 고민해두어야 한다. 이 이후의 내용은 자바의 내용이라기보다는 운영체제의 내용에 가깝기 때문에 추후에 운영체제에 대한 포스팅을 할 기회가 있으면 작성하도록 할것이다.&lt;/p&gt;
&lt;h1&gt;8. 싱글턴 패턴에서의 Thread-safe (Option)&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;디자인 패턴 중, 단 하나의 공유 객체를 만들어 재사용하는 패턴을 싱글턴패턴이라고 한다.&lt;/li&gt;
&lt;li&gt;싱글턴 패턴은 Thread-safe를 고려하지 않으면 싱글턴 패턴이 아니게될 가능성이 있다.&lt;/li&gt;
&lt;li&gt;따라서 싱글턴패턴을 Thread-safe하게 만드는 방법을 추가적으로 정리해보았다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;8-1.synchronized 사용&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;public class Singleton {
    private static Singleton instance;

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;문제점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;7장에서 말한 것과 같이 스레드 문제로부터 안전해졌지만 불필요한 락이 많아 성능상의 이슈가 발생할 확률이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;8-2.Double-Checkd Locking 사용&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package study.moon.Test;

public class Singleton {
    private static volatile Singleton instance;

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;volatile 키워드&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;사실 우리는 메인 메모리에 항상 직접 접근하여 연산을 하는것이 아니라 성능상의 이익을 얻기 위해 cpu 캐시에 저장된 값으로 연산을 진행한다. 그래서 멀티쓰레드 환경에서는 특정 쓰레드에서 값을 변경하여도 cpu캐시에서만 값이 변경되어, 메인 메모리에는 반영이 되지 않아 다른 쓰레드에서 문제가 발생할 수 있다. 이 때, volatile 키워드를 사용하면 해당 값을 cpu캐시에 갱신하지 않고 직접 메인 메모리에 갱신하게된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;문제점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;역시 아직 성능상의 문제가 있다.&lt;/li&gt;
&lt;li&gt;코드 가독성이 떨어진다.&lt;/li&gt;
&lt;li&gt;1.4 이하의 버전에서는 적용할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;8-3.Eager Initialization&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package study.moon.Test;

public class Singleton {
    /*
    static -&amp;gt; 인스턴스화와 상관없이 접근 가능
    private -&amp;gt; 외부에서 instance 바로 접근 방지
     */
    private static Singleton instance = new Singleton();

    /*
    private -&amp;gt; new Singleton() 방지
     */
    private Singleton() {}

    //오로지 해당 메서드를 통해서만 인스턴스 접근 가능
    public static Singleton getInstance() {
        return instance;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;문제점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;싱글턴 객체 사용 유무에 관계없이 클래스가 로딩되는 시점에 객체가 생성되어 메모리를 잡아먹을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;8-4.Lazy Initialization&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package study.moon.Test;

public class Singleton {

    private static Singleton instance;
    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) { // 처음으로 호출될 때 메모리에 할당한다.
            instance = new Singleton();
        }
        return instance;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;문제점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Thread-safe하지 않다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;8-5.Holder&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package study.moon.Test;

public class Singleton {

    private Singleton() {}

    private static class Holder{//getInstance()가 호출되기 전에는 참조되지 않는다.
        private static final Singleton instance = new Singleton();//클래스 로딩 시점에 한번만 호출, 재할당 금지
    }

    public static Singleton getInstance() {
        return Holder.instance;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;많이 사용되는 방법이라고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;8-5.Enum&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package study.moon.Test;

public enum Singleton {
    instance
}&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;간단하게 구현할 수 있다. enum의 특성을 이용하여 싱글턴 구현이 가능하다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Java&amp;amp;Spring/STUDY HALLE</category>
      <category>dead-lock</category>
      <category>Java</category>
      <category>Runnable</category>
      <category>singleton</category>
      <category>Thread</category>
      <author>무토(MUTO)</author>
      <guid isPermaLink="true">https://leemoono.tistory.com/26</guid>
      <comments>https://leemoono.tistory.com/26#entry26comment</comments>
      <pubDate>Wed, 20 Jan 2021 07:19:31 +0900</pubDate>
    </item>
    <item>
      <title>[Java] 예외처리</title>
      <link>https://leemoono.tistory.com/24</link>
      <description>&lt;h1&gt;0. 학습 목표&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;자바에서 예외 처리 방법(try, catch, throw, throws, finally)&lt;/li&gt;
&lt;li&gt;자바가 제공하는 예외 계층 구조&lt;/li&gt;
&lt;li&gt;Exception과 Error의 차이&lt;/li&gt;
&lt;li&gt;RuntimeException과 RuntimeException이 아닌것의 차이는?&lt;/li&gt;
&lt;li&gt;커스텀한 예외 만드는 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;1. 자바에서의 예외처리방법&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Java에서 Exception이란 &lt;b&gt;&lt;i&gt;프로그램의 실행 도중에 발생하는 문제상황&lt;/i&gt;&lt;/b&gt; 을 의미한다.&lt;/li&gt;
&lt;li&gt;if문을 사용하여 예외 처리를 진행할 수도 있지만 if문은 프로그램의 로직을 작성하는 부분도 존재하기 때문에 &lt;b&gt;&lt;i&gt;if문으로 예외처리를 진행할 시, 예외사항이 발생하는곳을 찾아보기 어렵다는 단점이 있다&lt;/i&gt;&lt;/b&gt;. 따라서 예외처리 구문을 통해 예외를 처리하는것이 바람직하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1-1. try - catch - finally&lt;/h2&gt;
&lt;h3&gt;Main.java&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] arr;
        try {
            arr = new int[n];
        }catch (NegativeArraySizeException e) {
            System.out.println(&quot;Exception occur&quot;);
        } finally {
            System.out.println(&quot;finally&quot;);
        }
        System.out.println(&quot;Hello Java Exception!!!&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1-1-1. try&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;실행하고싶은 로직을 작성하는 공간이다. 내가 작성하려고 했던 코드를 try 이후의 블록에 넣으면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1-1-2. catch(Exception e)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;try 구문에서 Exception이 발생했을때 실행되는 부분이다.&lt;/li&gt;
&lt;li&gt;발생할 예외클래스의 타입을 매개변수로 주입한다. 그 후 블록에 예외가 발생했을 때 필요한 로직들을 작성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;catch 구문은 여러번 중복하여 작성할 수도 있다.&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;package study.moon.Atest;

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] arr;
        try {
            arr = new int[n];
        }catch (NegativeArraySizeException e) {
            System.out.println(&quot;Exception occur1&quot;);
        }catch (IndexOutOfBoundsException e) {
            System.out.println(&quot;Exception occur2&quot;);
        }catch (NullPointerException e) {
            System.out.println(&quot;Exception occur3&quot;);
        } finally {
            System.out.println(&quot;finally&quot;);
        }
        System.out.println(&quot;Hello Java Exception!!!&quot;);


    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;catch 구문에서 사용할 코드들이 같다면 '|' 를 사용하면 하나의 블록에서도 표현이 가능하다.&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;package study.moon.Atest;

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] arr;
        try {
            arr = new int[n];
        }catch (NegativeArraySizeException | IndexOutOfBoundsException | NullPointerException e) {
            System.out.println(&quot;Exception occur1&quot;);
        } finally {
            System.out.println(&quot;finally&quot;);
        }
        System.out.println(&quot;Hello Java Exception!!!&quot;);

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1-1-3. finally&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;try에 진입했다면 무조건 실행되는 부분이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;굳이 필요없는기능같은데...??? 생략해도 되지 않을까요?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;return을 읽어도 finally는 동작한다.&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;package study.moon.Atest;

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] arr;
        try {
            arr = new int[n];
            return;          //이렇게 return을 하더라도
        }catch (NegativeArraySizeException | IndexOutOfBoundsException | NullPointerException e) {
            System.out.println(&quot;Exception occur1&quot;);
        } finally {
            System.out.println(&quot;finally&quot;);//finally는 읽어야지
        }
        System.out.println(&quot;Hello Java Exception!!!&quot;);

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;뿐만 아니라 이러한 finally는 DB 혹은 File 처리를 진행할 때 예외가 발생한 상황에 close()를 반드시 호출해주어야하는데 그런 작업을 할 때에도 유용하게 사용된다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;또한 finally는 사실 생략해도 된다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;finally 생략&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;package study.moon.Atest;

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] arr;
        try {
            arr = new int[n];
            return;
        }catch (NegativeArraySizeException | IndexOutOfBoundsException | NullPointerException e) {
            System.out.println(&quot;Exception occur1&quot;);
        }
        System.out.println(&quot;There is no finally!!!&quot;);

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1-1-4. printStackTrace()&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;예외 발생 당시의 호출 스택에 있었던 메서드의 정보와 예외 메시지를 화면에 출력한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1-1-5. getMassage()&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;발생한 예외 클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1-2. throw, throws&lt;/h2&gt;
&lt;h3&gt;1-2-1. throw&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;내가 임의로 예외를 발생시킬 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;임의의 예외발생&lt;/h3&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;package study.moon.Atest;

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        if (n == 1) {
            throw new IndexOutOfBoundsException();
        } else if (n==2) {
            throw new NegativeArraySizeException();
        } else if (n==3) {
            throw new IllegalArgumentException();
        }
        System.out.println(&quot;Hello Throw&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1-2-2. throws&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;해당 메서드를 호출한 상위 메서드로 예외를 위임한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;throws 뒤에 해당 메서드를 호출한 메서드로 위임할 예외를 작성한다.&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;package study.moon.Atest;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;

public class Main {
    public static void main(String[] args) throws IOException {//readLine()메서드가 throws IOException을 포함하고 있기 때문에 메인메서드에서 jvm 으로 예외처리를 위임한다.
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        int n = Integer.parseInt(br.readLine());//readLine()메서드가 throws IOException을 포함하고 있다.
        bw.write(n+&quot;&quot;);
        br.close();
        bw.flush();
        bw.close();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;try-with-resources&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;만약 finally를 close를 호출해야 하기 때문에 사용한다면 이 문법을 사용하는것이 좋다.&lt;br /&gt;해달 클래스가 AutoClosable이라는 인터페이스를 구현했다면 사용가능하다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;기존의 문법&lt;/h3&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        String str;
        try{
            str = br.readLine();
        } catch (IOException e) {
            e.getStackTrace();
        }finally {
            br.close();
            bw.close();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;새로 추가된 문법&lt;/h3&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;package study.moon.Atest;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
    public static void main(String[] args) throws IOException {
        try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {
            String str;
            str = br.readLine();
        } catch (IOException e) {
            e.getStackTrace();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;2. 자바가 제공하는 예외 계층 구조&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m6aiY/btqTJUepwm8/TxJEq20caaK101ip5oL3F1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m6aiY/btqTJUepwm8/TxJEq20caaK101ip5oL3F1/img.png&quot; data-alt=&quot;https://madplay.github.io/post/java-checked-unchecked-exceptions&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m6aiY/btqTJUepwm8/TxJEq20caaK101ip5oL3F1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm6aiY%2FbtqTJUepwm8%2FTxJEq20caaK101ip5oL3F1%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://madplay.github.io/post/java-checked-unchecked-exceptions&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;3. Error VS Exception&lt;/h1&gt;
&lt;h2&gt;3-1. Error&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;개발자가 미리 예측하여 처리할 수 없는 심각한 오류. 프로세스가 종료됨.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;3-2. Exception&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;개발자가 예측하여 문제가 발생하지 않도록 예외처리를 해주어야 하는 오류.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;ClassNotFoundException VS NoClassDefFoundError&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;비슷해 보이지만 문제를 해결하려면 하나는 골때리고 하나는 해결하기 쉬운 문제이다.&lt;br /&gt;전자는 이전에 공부한 것처럼 클래스로더가 클래스패스에서 해당 클래스를 찾지 못하면 발생하는 예외이다. 간단하게 클래스패스에 해당 클래스가 있는지 확인해주면 된다.&lt;br /&gt;후자는 컴파일타임때 클래스패스에 클래스가 존재하지만 런타임때 클래스가 변경되거나 클래스의 스태틱 초기화가 예외를 던질 경우에 해당 에러가 발생한다. 따라서 해당 클래스를 의존하는 모든 관계를 살펴봐야한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;NoClassDefFoundError 예제&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3x1WN/btqTJmCoHjQ/9Le2dYnVwzgF9wHWu9IGm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3x1WN/btqTJmCoHjQ/9Le2dYnVwzgF9wHWu9IGm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3x1WN/btqTJmCoHjQ/9Le2dYnVwzgF9wHWu9IGm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3x1WN%2FbtqTJmCoHjQ%2F9Le2dYnVwzgF9wHWu9IGm1%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;- 해당 소스를 컴파일하고 Test.class를 삭제하고 실행시키면 다음과 같은 상황이 발생한다.&lt;/p&gt;
&lt;h1&gt;4. RuntimeException VS !RuntimeException&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;말그대로 런타임에 발생하는 예외와 런타임에 발생하지 않고 컴파일타임에 발생하는 예외의 차이가 있다.&lt;br /&gt;따라서 런타임 예외는 따로 예외처리를 해주지 않아도 된다.&lt;br /&gt;반면에 비 런타임 예외는 예상할 수 있는 예외이기 때문에 예외처리를 반드시 해주어야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;5. 커스텀한 예외 만드는 방법&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;자바에서는 Exception 클래스를 상속받아 내가 원하는 예외처리를 추가적으로 만들어낼 수 있다.&lt;br /&gt;&quot;Hello World&quot;를 입력하면 예외를 발생시키는 프로그램을 만들어보자.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;HelloWorldException 예제&lt;/h3&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;package study.moon.Atest;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        String inputText = br.readLine();
        if (inputText.equals(&quot;Hello World&quot;)) {
            throw new HelloWorldException(&quot;헬로월드 작성하지 마세요&quot;);
        }
        bw.write(inputText);
        bw.flush();
        bw.close();
        br.close();
    }
}

class HelloWorldException extends RuntimeException {

    public HelloWorldException(String message) {
        super(message);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;결과&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4zAx9/btqTDcVrRJm/2TyNSPSs8Amjf6uXfVDsOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4zAx9/btqTDcVrRJm/2TyNSPSs8Amjf6uXfVDsOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4zAx9/btqTDcVrRJm/2TyNSPSs8Amjf6uXfVDsOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4zAx9%2FbtqTDcVrRJm%2F2TyNSPSs8Amjf6uXfVDsOK%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Java&amp;amp;Spring/STUDY HALLE</category>
      <category>error</category>
      <category>Exception</category>
      <category>Java</category>
      <category>java error</category>
      <category>Java Exception</category>
      <category>study halle</category>
      <category>예외</category>
      <author>무토(MUTO)</author>
      <guid isPermaLink="true">https://leemoono.tistory.com/24</guid>
      <comments>https://leemoono.tistory.com/24#entry24comment</comments>
      <pubDate>Fri, 15 Jan 2021 22:07:16 +0900</pubDate>
    </item>
  </channel>
</rss>