1.   제네릭

제네릭 Generics은 입력되는 값의 타입을 자유롭게 사용하기 위한 도구

📌 다음은 실제 MutableList의 선언부

public interface MutableList<E> : List<E>, MutableCollection<E>


  ✓  클래스명 옆에 <E>부분에 String 같은 특정 타입이 지정되면 클래스 내부에 선언된 모든 E에 String 타입으로 지정
  ✓  결과적으로 var list: Array<E>가 var list: Array<String>으로 변경이 되는 것

  ⚡️  이렇게 설계된 클래스는 주로 구현하는 용도로 사용하며 컬렉션이나 배열에서 입력되는 값의 타입을 특정하기 위해 다음과 같이 사용

var list: MutableList<String> = mutableListOf("abc", "def", "ghi") 
fun testGenerics() {
    // String으로 제네릭을 사용했기 때문에 list 변수에는 문자열만 담을 수 있음.
    var list: MutableList<String> = mutableListOf()
    list.add("abc")
    list.add("def")
    list.add("ghi")
//    list.add(30) // 입력 오류가 발생

    // String 타입의 item 변수로 꺼내서 사용할 수 있음.
    for (item in list) {
        println("리스트에 저장된 값은 ${item}입니다")
    }
}

 


2.  Null

코틀린은 Null 값의 처리에 많은 공을 들인 언어. null은 프로그래밍하면서 항상 이슈의 중심에 있는데 null로 인해 프로그램 전체, 혹은 앱 자체가 멈출 수 있기 때문.

 

 프로그램이 멈출 수 있는 상황 예시

   

      main() 함수 안에 one 변수를 하나 선언하고 타입으로 클래스를 지정.
     그리고 특정 조건이 만족할 때만 선언한 변수에 생성자를 호출해서 저장하는 조건문 if를 만듦.
     그리고 변수를 통해 해당 클래스의 메서드를 호출.

class One {
    fun printMe() {
        print("null safety")
    }
}

fun main() {
    var one: One
    if (1 > 2) {
        one = One()
    }
 //   one.printMe()
}

 

  👾  이 코드에서 조건이 false이기 때문에 one 변수는 아무것도 없는 null 상태가 됨
         ➡️  print 메서드를 호출하면 null 포인터 예외가 발생하면서 프로그램이 다운, 물론 IDE에서 오류를 발생시켜 컴파일되지 않도록 막아줌
  👾  코드양이 많아 지면 이런 상황이 언제든지 발생할 수 있는데, 코틀린은 이런 상황을 방지하기 위해서 안전장치를 마련해둠
         ➡️  그 결과물이 Null Safety


1)  null 값 허용하기 : ?


코틀린에서 지정하는 기본 변수는 모두 null이 입력되지 않음
null 값을 입력하기 위해서는 변수를 선언할 때 타입 뒤에 ? (Nullable, 물음표)를 입력.

var variable : String?

 

 

변수에 null 허용하기

변수의 타입 뒤에 물음표를 붙이지 않으면 null 값을 입력할 수 없음.
null 예외를 발생시키고 싶지 않다면 기본형으로 선언.
fun main() {
    var nullable: String? // 타입 다음에 물음표를 붙여서 null 값을 입력할 수 있음.
    nullable = null

    var notNullable: String
//    notNullable = null // 일반 변수에는 null을 입력할 수 없음.
}

함수 파라미터에 null 허용 설정하기

함수의 파라미터에도 null 허용 여부를 설정할 수 있음.
함수의 파라미터가 null을 허용하려면 해당 파라미터에 대해서 null 체크를 먼저 해야만 사용할 수 있음.
단, 파라미터를 조건문에서 null인지 아닌지를 체크해야만 사용할 수 있음
fun nullParameter(str: String?) {
    // 파라미터 str에 null이 허용되었기 때문에 함수 내부에서 null 체크를 하기 전에는 str을 사용할 수 없음.
    if (str != null) {
        var length = str.length
    }
}

함수의 리턴 타입에 null 허용 설정하기

함수의 리턴 타입에도 물음표를 붙여서 null 허용 여부를 설정할 수 있음.
함수의 리턴 타입에 Nullable이 지정되어 있지 않으면 null 값을 리턴할 수 없음.
fun nullReturn(): String? {
    return null
}

 

2)  안전한 호출 : ?.


변수를 Nullable로 만들기 위해서 물음표를 사용.
?.(Safe Call, 물음표와 점)을 사용하면 null 체크를 간결하게 할 수 있음
  ✓  Nullable인 변수 다음에 ?.을 사용하면 해당 변수가 null일 경우 ?. 다음의 메서드나 프로퍼티를 호출하지 않음

fun testSafeCall(str: String?): Int? {
    // 문자열 길이를 반환하는 length 프로퍼티를 호출했는데 str 변수 자체가 null일 경우는 
    // length 프로퍼티를 호출하지 않고 null을 반환.

    var result: Int? = str?.length
    return result
}

3)  Null 값 대체하기 : ?:


?:(Elvis Operator, 물음표와 콜론)을 사용하면 원본 변수가 null 일 때 넘겨줄 기본값을 설정할 수 있음

 

다음 코드에서 Safe Call 다음에 호출되는 프로퍼티 뒤에 다시 ?:을 붙이고 0이라는 값을 표시.
이렇게 호출하면 str 변수가 null일 경우 가장 뒤에 표시한 0을 반환.
fun testElvis(str: String?): Int {
    // length 오른쪽에 ?:을 사용하면 null일 경우 ?: 오른쪽의 값이 반환.
    var resultNonNull: Int = str?.length ?: 0
    return resultNonNull
}

 


 

💫  물음표의 위치와 형태에 따라서 Nullable, Safe Call, Elvis Operator가 구분

🤓  Nullable

표기법 : 선언하는 변수의 타입 다음에 ? 표기.
사용 목적 : null을 입력받기 위해 사용.
사용 예 : var nullable: String?
🥸  Safe Call

표기법 : 선언된 변수의 이름 다음에 ?. 표기.
사용 목적 : null 일 때 ?. 다음에 나오는 속성이나 명령어를 처리하지 않기 위해 사용.
사용 예 : var result = 변수?.length
🧐  Elvis Operator

표기법 : 선언된 변수의 이름 다음에 ?: 표기.
사용 목적 : null일 때 ?: 다음에 나오는 값을 기본 값으로 사용.
사용 예 : var result = 변수 ?: 0

 

 

 

[ 내용 참고 : IT 학원 강의 ]


1. 접근 제한자

코틀린에서 정의되는 클래스, 인터페이스, 메서드, 프로퍼티는 모두 접근 제한자 Visibility Modifiers를 가질 수 있음.
함수형 언어라는 특성 때문에 코틀린은 기본 객체지향에서 접근 제한자의 기준으로 삼았던 패키지 대신에 모듈 개념이 도입

💡 코틀린에서 모듈이란?
    -  코틀린에서 모듈이란 한 번에 같이 컴파일되는 모든 파일을 말함.
        안드로이드를 예로 든다면 하나의 앱이 하나의 모듈이 될 수 있음. 또한 라이브러리도 하나의 모듈.


1) 접근 제한자의 종류


  🐰  접근 제한자는 서로 다른 파일에게 자신에 대한 접근 권한을 제공하는 것인데 각 변수나 클래스 이름 앞에 아무런 예약어를 붙이지
않았을 때는 기본적으로 public 접근 제한자가 적용

 

각각의 접근 제한자가 공개하는 범위
  • public : 코틀린의 기본 접근 제한자. 어디서나 접근 가능
  • internal : 같은 모듈 내에서 접근 가능. 안드로이드 개발 시에는 한 프로젝트 안에 있으면 같은 모듈
                    만약 한 프로젝트에 여러 모듈을 만든다면 이는 모듈 간 접근이 제한
  • protected : 자식 클래스에서는 접근할 수 있음
  • private : 해당 클래스 내부에서만 접근할 수 있음

2) 접근 제한자의 적용


  🐰  접근 제한자를 붙이면 해당 클래스, 멤버 프로퍼티 또는 메서드에 대한 사용이 제한

open class Parent {
    private val privateVal = 1
    protected open val protectedVal = 2
    internal val internalVal = 3
    val defaultVal = 4
}

class Child: Parent() {
    fun callVariables() {
 //       println(privateVal) // 호출이 안됨.
        println(protectedVal) // 상속 관계이므로 접근할 수 있음.
        println(internalVal)  // 동일한 모듈이므로 접근할 수 있음
        println(defaultVal)
    }
}

// 상속 관계가 아닌 외부 클래스에서 Parent 클래스를 생성하고 사용.
// 상속 관계가 아니기 때문에 public과 internal 에만 접근할 수 있음
class Stranger {
    fun callVariables() {
        val parent = Parent()
//        println(parent.privateVal) // 호출이 안됨.
//        println(parent.protectedVal)  // 호출이 안됨.
        println(parent.internalVal)
        println(parent.defaultVal)
    }
}

 

 

 

[ 내용 참고 : IT 학원 강의 ]


1.  패키지

🚀  패키지는 클래스와 소스 파일을 관리하기 위한 디렉토리 구조의 저장 공간
      ✓ 현재 클래스가 어떤 패키지(디렉토리)에 있는지 표시
      ✓ 디렉토리가 계층 구조로 만들어져 있으면 점(.)으로 구분해서 각 디렉토리를 모두 나열해 줌

package 메인 디렉토리. 서브 디렉토리
class 클래스 {
}


🚀  이 디렉토리 구조라면 윈도우의 파일 탐색에서 보면 메인 디렉토리 아래에 서브 디렉토리가 있고, 서브 디렉토리 안에 실제 코드가 있는 클래스.kt 파일이 있음

🚀  하나의 패키지에 여러 개의 파일을 생성할 수 있기 때문에 '서로 관계가 있는 파일을 동일한 패키지로 만들어 두면 관리가 용이

 


2.  추상화

🚀  프로그래밍을 하기 전 개념 설계를 하는 단계에서는 클래스의 이름과 클래스 안에 있음 직한 기능을 유추해서 메서드 이름으로 먼저 나열. 이때 명확한 코드는 설계 단계에서 메서드 블록 안에 직접 코드를 작성하는데, 그렇지 않은 경우에는 구현 단계에서 코드를 작성하도록
메서드의 이름만 작성  ▶️ 이것을 추상화 Abstract 라고 하며 abstract 키워드를 사용해서 명시

 

  ✓ 구현 단계에서는 이 추상화된 클래스를 상속받아서 아직 구현되지 않은 부분을 마저 구현
  ✓ 다음과 같이 추상화된 Aniamal 클래스를 만들고 동물이 사용할 것 같은 기능 중 walk()와 move()를 설계한다고 가정

abstract class Animal {
    fun walk() {
        println("Walking")
    }

    abstract fun move()
}

 

  👾 walk는 명확하게 걸어가는 행위이지만 move는 어떤 동물이냐에 따라서 달라진다고 가정
  👾 이렇게 앞으로 상속받을 자식 클래스의 특징에 따라 코드가 결정될 가능성이 있다면 해당 기능도 abstract 키워드로 추상화
        ➡️  실제 구현 클래스는 이 추상 클래스를 상속받아서 아직 구현되지 않은 추상화되어 있는 기능을 모두 구현해 줌
  👾 추상 클래스는 독립적으로 인스턴스화 할 수 없기 때문에 구현 단계가 고려되지 않는다면 잘못된 설계가 될 수 있음

class Bird : Animal() {
    override fun move() {
        println("Bird move")
    }
}

 


3.  인터페이스

🚀 인터페이스 interface는 실행코드 없이 메서드 이름만 가진 추상 클래스. 즉, 누군가 설계해 놓은 개념 클래스 중에 실행 코드가 한 줄이라도 있으면 추상 클래스, 코드 없이 메서드 이름만 나열되어 있으면 인터페이스

🚀 인터페이스는 안드로이드에서는 주로 상속 관계의 설계보다는 외부 모듈에서 내가 만든 모듈을 사용할 수 있도록 메서드의 이름을 나열해둔 일종의 명세서로 제공

🚀 인터페이스는 interface 예약어를 사용해서 정의할 수 있고 인터페이스에 정의된 메서드를 오버라이드해서 구현할 수 있음
    ✓  코틀린은 프로퍼티도 인터페이스 내부에 정의할 수 있고, 추상 클래스와 다르게 class 키워드는 사용되지 않음

interface 인터페이스명 {
    var 변수 : String
    fun 메서드1()
    fun 메서드2()
}

 

 

1) 인터페이스 만들기

 

interface 예약어로 인터페이스를 정의
코틀린은 인터페이스 내부에 프로퍼티도 정의할 수 있음

interface InterfaceKotlin {
    var variable: String
    fun get()
    fun set()
}

 

 

2) 클래스에서 구현하기


인터페이스를 클래스에서 구현할 때는 상속과는 다르게 생성자를 호출하지 않고 인터페이스 이름만 지정해 주면 됨

class KotlinImpl : InterfaceKotlin {
    override var variable: String = "Default"
    override fun get() {

    }
    override fun set() {

    }
}

 

💡  인터페이스를 코틀린의 상속 형태가 아닌 소스 코드에서 직접 구현할 때도 있는데, object 키워드를 사용해서 구현해야 함
       ➡️  안드로이드 프로젝트에서 자주 사용하는 형태

val kotlinImpl = object : InterfaceKotlin {
    override var variable: String = "Default"

    override fun get() {

    }

    override fun set() {

    }
}

 

 

 

[ 내용 참고 : IT 학원 강의 ]


1.  MyBatis 

스프링 프레임워크의 중요한 특징 중 하나는 다른 프레임워크들을 쉽게 결합해서 사용할 수 있다는 점이다. 이것은 스프링 프레임워크가 웹이나 데이터베이스와 같이 특정한 영역을 구애받지 않고 시스템의 객체지향 구조를 만드는데 이용된다는 성격 때문.

 

데이터베이스와 관련해서 스프링 프레임워크는 자체적으로 'spring-jdbc'와 같은 라이브러리를 이용해서구현할 수도 있고, MyBatisJPA 프레임워크를 이용하는 방식도 존재


💫  MyBatis는 'Sql Mapping Framework'라고 표현

  • 'Sql Mapping'이라는 단어가 의미하는 바는 SQL의 실행 결과를 객체지향으로 '매핑'해 준다는 뜻
  •  MyBatis를 이용하면 기존의 SQL을 그대로 사용할 수 있고 다음과 같은 점들이 편리해짐
  PreparedStatement / ResultSet의 처리 : 기존에 프로그램을 작성해서 하나씩 처리해야 하는 파라미터나 ResultSet의 getXXX()를 MyBatis가 알아서 처리해 주기 때문에 기존에 비해서 많은 양의 코드를 줄일 수 있음
  Connection / PreparedStatement / ResultSet의 close() 처리 : MyBatis와 스프링을 연동해서 사용하는 방식을 이용하면 자동으로 close() 처리할 수 있음
  SQL의 분리 : MyBatis를 이용하면 별도의 파일이나 어노테이션 등을 이용해서 SQL을 선언함. 파일을 이용하는 경우에는 SQL을 별도의 파일로 분리해서 운영이 가능  ➡️ 자바 코드에 쿼리문을 넣지 않아도 됨

 

1)  MyBatis와 스프링의 연동 방식


👻  MyBatis는 단독으로도 실행이 가능한 완전히 독립적인 프레임워크이지만, 스프링 프레임워크는 MyBatis와 연동을 쉽게 처리할 수 있는 라이브러리와 API들을 제공
👻  스프링에서 제공하는 라이브러리를 이용하는지 여부에 따라서 다음과 같은 방식 중에 하나로 개발이 가능

  • MyBatis를 단독으로 개발하고 스프링에서 DAO를 작성해서 처리하는 방식
    ➡️  기존의 DAO에서 SQL의 처리를 MyBatis를 이용하는 구조로써 완전히 MyBatis와 스프링 프레임워크를 독립적인 존재로 바라보고 개발하는 방식
  • MyBatis와 스프링을 연동하고 Mapper 인터페이스만 이용하는 방식
    ➡️  스프링과 MyBatis 사이에 'mybatis-spring'이라는 라이브러리를 이용해서 스프링이 데이터베이스 전체에 대한 처리를 하고
    MyBatis는 일부 기능 개발에 활용하는 방식. 개발 시에는 Mapper 인터페이스라는 방식을 이용해서 인터페이스만으로 모든 개발이 가능

 

2) MyBatis를 위한 라이브러리들


👻  MyBatis를 이용하려면 다음과 같은 라이브러리들이 필요

스프링 관련 : spring-jdbc, spring-tx
MyBatis 관련 : mybatis, mybatis-spring
  ➡️  다른 스프링 관련 라이브러리들과 버전이 같도록 통일해서 추가


    📌  MyBatis 관련 라이브러리는 검색을 이용해서 추가. MyBatis 버젼과 mybatis-spring 라이브러리의 버전은 일치하지 않으므로 주의해서 사용

 



3) MyBatis를 위한 스프링 설정 - SqlSessionFactory


👻  MyBatis를 이용하기 위해서는 스프링에 설정해둔 HikariDataSource를 이용해서 SqlSessionFactory라는 빈을 설정
👻  root-context.xml에 'mybatis-spring' 라이브러리에 있는 클래스를 이용해서 <bean>을 등록

 

 


2. Mapper 인터페이스 활용하기

MyBatis는 SQL 파일을 별도로 처리할 수 있지만 인터페이스와 어노테이션으로도 처리가 가능

 

📌  프로젝트에 mapper라는 이름의 패키지를 구성하고 현재 시간을 처리하는 TimeMapper 인터페이스를 선언

 


🎃  TimeMapper는 데이터베이스의 현재 시각을 문자열로 처리하도록 구성

🎃  MyBatis에는 @Select 어노테이션을 이용해서 쿼리를 작성할 수 있는데, JDBC와 마찬가지로 ";"을 이용하지 않으므로 주의

 

 

 

🎃  작성된 인터페이스를 매퍼(Mapper) 인터페이스라고 하는데 마지막으로 어떠한 매퍼 인터페이스를 설정했는지 root-context.xml에 등록해 주어야 함
🎃 root-context.xml에는 <mybatis:scan> 태그를 이용해서 매퍼 인터페이스의 설정을 추가
🎃  root-context.xml 파일 상단의 xmlns, xsi 설정에 mybatis-spring 관련 설정이 추가 되어야 함.




 

1)  테스트 코드를 통한 확인


👾  test 폴더에 mapper 패키지와 TimeMapperTests 테스트 클래스를 작성해서 확인

import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import static org.junit.jupiter.api.Assertions.*;

@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
class TimeMapperTest {
    @Autowired(required = false)
    private TimeMapper timeMapper;

    @Test
    public void testGetTimer() {
        log.info(timeMapper.getTime());
    }

}


  📍  테스트 코드에서는 @Autowired 내에 required 속성을 지정
      ✓  @Autowired(required = false)로 지정하면 해당 객체를 주입 받지 못하도라도 예외가 발생하지 않는데 인텔리제이의 경우@Service, @Repository 와 같이 직접 스프링의 빈으로 등록된 경우가 아니라면 경고가 발생하므로 이를 방지하기 위해서 사용

 


3.  XML로 SQL 분리하기

👩🏻‍💻  MyBatis를 이용할 때 SQL은 @Select와 같은 어노테이션을 이용해서 사용하기도 하지만 대부분은 SQL을 별도의 파일로 분리하는 것을 권장
👩🏻‍💻  XML을 이용하는 이유는 SQL이 길어지면 이를 어노테이션으로 처리하기가 복잡해지고, 어노테이션이 나중에 변경되면 프로젝트 전체를 다시 빌드하는 작업이 필요하기 때문에 단순 파일로 사용하는 것이 편리

 

XML과 매퍼 인터페이스를 같이 결합할 때는 다음과 같은 과정으로 작성
  1. 매퍼 인터페이스를 정의하고 메소드를 선언
  2. 해당 XML 파일을 작성 (파일 이름과 매퍼 인터페이스 이름을 같게)하고 <select>와 같은 태그를 이용해서 SQL을 작성
  3. <select>, <insert> 등의 태그에 id 속성 값을 매퍼 인터페이스의 메소드 이름과 같게 작성



🎃  TimeMapper2 매퍼 인터페이스를 정의
🎃  어노테이션이 없는 getNow() 메서드만을 작성

 

 

 

🎃  main/resources 폴더에 mapper 폴더를 추가

 

🎃  TimeMapper2.xml을 다음과 같이 작성 (매퍼 인터페이스와 같은 이름으로 대소문자 주의)
  ➡️ 스프링 버전이 업그레이드 되면서 인터페이스와 xml의 이름을 똑같이 하지 않아도 됨

  ➡️ 하지만 관리상의 목적으로 매퍼 인터페이스와 xml의 이름을 같게 하기도 함
🎃  <mapper> 태그의 namespace 속성에서 매퍼 인터페이스를 맞게 지정하면 됨
  ➡️ <mapper> 태그의 namespace 속성을 반드시 매퍼 인터페이스의 이름과 동일하게 지정

 

  🎃  <select> 태그는 반드시 resultType이나 resultMap이라는 속성을 지정
         ➡️  resultType은 말 그대로 select 문의 결과를 어떤 타입으로 처리할지에 대한 설정으로 java.lang.String과 같이 전체 이름을 써야 하지만 자주 사용하는 타입은 string과 같이 사용할 수 있음

 


🎃  마지막으로 root-context.xml에 있는 MyBatis 설정에 XML 파일들을 인식하도록 설정을 추가

 

  🎃  추가된 mapperLocations는 말 그대로 'XML 매퍼 파일들의 위치'를 의미
  🎃  resources의 경우 'classpath:' 접두어를 이용해서 인식되는 경로이고 mapper 폴더 밑에 폴더가 있어도 관계없도록 '**'와 모든 '.xml'을 의미라는 '*.xml'을 지정

 

 

📌  XML 설정이 정상적인지 테스트를 통해서 확인. 기존에 만들어진 TimeMapperTests를 이용.



 

 

 

[ 내용 참고 : IT 학원 강의 ]

 

 


1.  스프링의 시작 

스프링 프레임워크는 웹이라는 제한적 용도로만 쓰이는 것이 아니라 객체지향의 의존성 주입 dependency injection 기법을 적용할 수 있는 객체지향 프레임워크

 

  •   스프링 프레임워크는 로드 존슨이 2002년도에 집필했던 'J2EE 설계 및 개발 wrox'이라는 책의 예제 코드에서 시작되었는데 말 그대로 효과적이고 가볍게 J2EE를 이용할 수 있다는 것을 증명하면서 예제의 코드를 발전시킨 것
  • 2000년 당시 자바 진영에서 JavaEE의 여러가지 스펙을 정의하고 비대해지는 동안 스프링 프레임워크는 반대로 '경량 light weight 프레임워크'를 목표로 만들어짐
  • 스프링이 등장할 때 여러 종류의 프레임워크들이 비슷한 사상(경량화, 단순화)으로 등장했지만 다른 프레임워크들과 달리 스프링 프레임워크는 개발과 설계 전반에 관련된 문제들을 같이 다루었기 때문에 결론적으로 가장 성공한 프레임워크로 기록
  • 스프링 프레임워크는 가장 중요한 '코어core' 역할을 하는 라이브러리와  여러 개의 추가적인 라이브러리를 결합하는 형태로 프로젝트를 구성. 가장 대표적으로 웹 MVC구현을 쉽게 할 수 있는 'Spring Web MVC'나 JDBC 처리를 쉽게 할 수 있는 'MyBatis'를 연동하는 'mybatis-spring'과 같은 라이브러리가 그러한 예. 

2.  의존성 주입 

스프링이 객체지향 구조를 설계할 때 받아들인 개념은 '의존성 주입 dependency injection'이라는 사상.

    ✓ 의존성 주입은 어떻게 하면 '객체와 객체 간의 관계를 더 유연하게 유지할 것인가?'에 대한 고민에서 나옴

    ✓ 즉 의존성이란 하나의 객체가 자신이 해야 하는 일을 하기 위해서 다른 객체의 도움이 필수적인 관계를 의미

    ✓ 스프링 프레임워크는 바로 이런 점을 프레임워크 자체에서 지원. 다양한 방식으로 필요한 객체를 찾아서 사용할 수 있도록 XML 설정이나 자바 설정 등을 이용

 


3.  ApplicationContext 와 빈 Bean

👻  서블릿이 존재하는 공간을 서블릿 콘텍스트 ServletContext라고 했던 것 처럼, 스프링에서는 빈 Bean 이라고 부르는 객체들을 관리하기 위해서 ApplicationContext 라는 존재를 활용

코드의 경우 ApplicationContext는 root-context.xml을 이용해서 스프링이 실행

ApplicationContext 객체가 생성

root-context.xml을 읽으면 SampleService와 SampleDAO가 해당 클래스의 객체를 생성해서 관리

테스트를 실행하면 @Autowired가 처리된 부분에 맞는 타입의 빈 Bean 이 존재하는지를 확인

이를 테스트 코드 실행 시에 주입

멤버변수에 직접 @Autowired를 선언하는 방식을 '필드 주입 Field Injection' 방식

 


4.  SampleDAO 주입하기 

 

@Autowired를 이용하면 필요한 타입을 주입받을 수 있다는 사실을 이용해서 SampleService를 다음과 같이 변경

 

 

 

📌  Lombok의 @ToString을 적용한 부분과 SampleDAO의 변수로 선언하고 @Autowired를 적용

📌  테스트 코드를 실행하면 SampleService 객체 안에 SampleDAO 객체가 주입된 것을 확인

 

 

 

 

 

👾  스프링을 이용할 때는 클래스를 작성하거나 객체를 직접 생성하지 않음

👾 이 역할은 스프링 내부에서 이루어지며 ApplicationContext가 생성된 객체들을 관리

  • 서블릿의 url 매핑의 경우 web.xml에 <servlet>이라는 태그를 이용해서 서블릿 클래스의 이름과 경로를 전부 기록해야 했지만, 최근에는 @WebServlet 어노테이션이 이를 대신하는 것처럼, 스프링도 비슷한 방식으로 발전, 초기 스프링 버전에서는 XML 파일에 <bean>이라는 것을 이용해서 설정하는 방식이 2.5버전 이후에 어노테이션 형태로 변화되면서 예전에 비해 편리하게 설정이 가능

5.  @Repository, @Service 

👩🏻‍💻  서블릿에서도 @WebServlet이나 @WebFilter와 같이 다양한 어노테이션이 존재하듯이 스프링 프레임워크는 애플리케이션 전체를 커버하기 때문에 다양한 종류의 어노테이션을 사용하도록 작성 

 

@Controller      : MVC의 컨트롤러를 위한 어노테이션
@Service           : 서비스 계층의 객체를 위한 어노테이션
@Repository    : DAO와 같은 객체를 위한 어노테이션
@Component   : 일반 객체나 유틸리티 객체를 위한 어노테이션

 

💡  DAO(Data Access Object)   "실제로 DB의 데이터에 접근하는 객체"
  •  DAO는 Service와 DB를 연결하는 역할을 하며, 실제로 DB에 접근하여 data를 삽입, 삭제, 조회, 수정 등 CRUD 기능을 수행

💡 DTO(Data Transfer Object)   "계층 간 데이터 교환을 위한 객체"

  • 로직을 가지지 않고 getter/setter 메소드만 가진 순수한 데이터 객체 클래스(Java Beans)로 DB에서 데이터를 얻어 Service나 Controller 등으로 보낼 때 사용

💡 VO(Value Object)   "변경 불가능하며 오직 읽기만 가능 (Read-Only)"

  • DTO는 setter를 가지고 있어 값을 변경할 수 있지만, VO는 getter만을 가지기 때문에 읽기만 가능하고 수정은 불가능

 

출처: https://hstory0208.tistory.com/entry/Spring-DAO-DTO-VO란-각각의-개념에-대해-알아보자 

 

 

👩🏻‍💻  스프링이 사용하는 어노테이션의 경우 웹 영역뿐만 아니라 애플리케이션 전체에 사용할 수 있는 객체들을 포함

👩🏻‍💻  어노테이션을 이용하게 되면 스프링 설정은 <bean>이 아니라 '해당 패키지를 조사해서 어노테이션들을 이용'하는 설정으로 변경

 

설정 변경 전과 후

 

📌  기존 설정과 비교해 보면 XML 위쪽의 xmlns 네임 스페이스가 추가되고 schemaLocation이 변경

📌  내용에는 component-scan이 추가되었는데 속성값으로는 패키지를 지정 

📌  'component-scan'은 해당 패키지를 스캔해서 스프링의 어노테이션들을 인식

 


 

 

 

🎃  SampleDAO는 해당 클래스의 객체가 스프링에서 빈 Bean으로 관리될 수 있도록 @Repository 라는 어노테이션을 추가

 

 

 

 

 

 

 

 

 

🎃  SampleService에는 @Service 어노테이션을 추가

 

 

 

 

 

 

 


 

생성자 주입 방식 

 

👾  초기 스프링에서는 @Autowired를 멤버 변수에 할당하거나, Setter를 작성하는 방식을 많이 이용해 왔지만, 스프링 3 이후에는 생성자 주입이라고 부르는 방식을 더 많이 활용.

👾  생성자 주입 방식은 객체를 생성할 때 문제가 발생하는 지를 미리 확인할 수 있기 때문에 필드 주입이나 Setter 주입 방식 보다 선호되는 방식 

👾  Lombok에서는 생성자 주입을 보다 간단하게 작성할 수 있는데 @RequiredArgsConstructor를 이용해서  필요한 생성자 함수를 자동으로 작성할 수 있음

 

 

 

 

 

💫  생성자 주입 방식은 다음과 같은 규칙으로 작성

   

    - 주입 받아야 하는 객체의 변수는 final로 작성

     - 생성자를 이용해서 해당 변수를 생성자의 파라미터로 지정

 

 

 

 

 

 

 


6.  인터페이스를 이용한 느슨한 결합 

스프링이 의존성 주입을 가능하게 하지만  '좀 더 근본적으로 유연한 프로그램을 설계하기 위해서는 인터페이스를 이용'해서 나중에 다른 클래스의 객체로 쉽게 변경할 수 있도록 하는 것이 좋음.

    ⚡️  예를 들어 코드에서 SampleDAO를 다른 객체로 변경하려면 결론적으로 SampleService 코드 역시 수정해야 함. 추상화된 타입을 이용하면 이러한 문제를 피할 수 있는데 가장 대표적인 것이 인터페이스. 인터페이스를 이용하면 실제 객체를 모르고 타입만을 이용해서 코드를 작성하는 일이 가능해 짐. 

 

1)  SampleDAO를 인터페이스로 변경하기  

 

 

 

🎃  클래스로 작성된 SampleDAO을 인터페이스로 수정. @Repository 삭제

🎃  SampleService는 SampleDAO라는 인터페이스를 보게 되었지만 코드상의 변경은 필요하지 않음

 

 

 

 

 

 

 

 

🎃 SampleDAO 인터페이스는 구현 코드가 없기 때문에 구현한 클래스를 SampleDAOImpl 이라는 이름으로 선언

🎃 SampleDAOImpl에는 @Repository를 이용해서 해당 클래스의 객체를 스프링의 빈 Bean으로 처리되도록 구성

 

 

 

 

 

SampleService의 입장에서는 인터페이스만 바라보고 있기 때문에 실제 객체가 SampleDAOImpl의 인스턴스인지 알수 없지만, 코드를 작성하는 데에는 아무런 문제가 없음. 이처럼 객체와 객체의 의존 관계의 실제 객체를 몰라도 가능하게 하는 방식을 '느슨한 결합 Loose coupling'이라고 함. 느슨한 결합을 이용하면 나중에 SampleDAO 타입의 객체를 다른 객체로 변경하더라도 SampleService 타입을 이용하는 코드를 수정할 일이 없기 때문에 보다 유연한 구조가 됨. 


2) 다른 SampleDAO 객체로 변경해 보기 

 

 

🎃  예를 들어 특정한 기간에만 SampleDAO를 다른 객체로 변경해야 되는 경우 EventSampleDAOImpl이라는 클래스를 작성

 

 

 

 

 

 

 

 

이렇게 되면 SampleService에 필요한 SampleDAO 타입의 빈 Bean이 두 개 SampleDAOImpl, EventSampleDAOImpl 가 되기 때문에 스프링 입장에서는 어떤 것을 주입해야 하는지 알 수 없음테스트 코드를 실행하면 어떤 클래스의 객체를 사용해야 하는지 알 수 없으므로 다음과 같은 에러가 발생

 

Error creating bean with name 'sampleService' defined in file [E:\work\dev\projects\projects-spring intellij\springex\build\classes\java\main\com\nomadlab\springex\sample\SampleService.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.nomadlab.springex.sample.SampleDAO' available: expected single matching bean but found 2: eventSampleDAOImpl,sampleDAOImpl 

 

 

스프링이 기대하는 것은 SampleDAO 타입의 객체가 하나 single 이길 기대했지만 2개가 발견. 

이를 해결하는 가장 간단한 방법은 두 클래스 중에 하나를 @Primary라는 어노테이션으로 지정해 주는 것

 


 

3) @Qualifier 이용하기 

 

👾  @Primary를 이용하는 방식 외에도 @Qualifier를 이용하는 방식도 있음

👾  @Qualifier는 이름을 지정해서 특정한 이름의 객체를 주입받는 방식

 

  📌  Lombok과 @Qualifier를 같이 이용하기 위해서는 src/main/java 폴더에 lombok.config 파일을 생성

 

 

 

 

🎃  동작을 확인하기 위해서 SampleDAOImpl과 EventSampleDAOImpl 에는 @Qualifier를 적용. 

🎃  SampleDAOImpl 에는 normal 이라는 이름을 지정 

 

 

 

 

 

 

 

🎃  EventSampleDAOImpl에는 event 라는 이름을 지정 

 

 

 

 

 

 

 

 

 

 

 

🎃  SampleService에는 특정한 이름의 객체를 사용하도록 수정

 

 

 

 

 

 

 

    🎃  normal 이름을 가진 SampleDAOImpl이 주입되는 것을 확인

 


4)  스프링의 빈 Bean 으로 지정되는 객체들 

 

👾  스프링 프레임워크를 이용해서 객체를 생성하고 의존성 주입을 이용할 수 있다는 사실을 알았지만 작성되는 모든 클래스의 객체가 스프링의 빈 Bean으로 처리되는 것은 아님

👾  스프링의 빈 Bean으로  등록되는  객체들은 쉽게 말해서 '핵심 배역'을 하는 객체들로 오랜 시간 동안 프로그램 내에 상주하면서 중요한 역할을 하는 '역할' 중심의 객체들

👾  반대로 DTO나 VO와 같이 '역할' 보다는 '데이터'에 중심을 두고 설계된 객체들은 스프링의 빈 Bean을 등록하지 않음

      특히 DTO의 경우 생명주기가 굉장히 짧고, 데이터 보관이 주된 역할이기 때문에 스프링에서 빈 Bean으로 처리하지 않음


5) XML이나 어노테이션으로 처리하는 객체 

 

👾  빈 Bean 으로 처리할 때 XML 설정을 이용할 수도 있고, 어노테이션을 처리할 수도 있지만, 이에 대한 기준은 '코드를 수정할 수 있는가'로 판단

👾  예를 들어 jar 파일로 추가되는 클래스의 객체를 스프링의 빈Bean으로 처리해야 한다면 해당 코드가 존재하지 않기 때문에 어노테이션을 추가할 수가 없다는 문제가 생김. 이러한 객체들은 XML에서 <bean>을 이용해서 처리하고, 직접 처리되는 클래스는 어노테이션을 이용하는 것이 좋음. 


7.   웹 프로젝트를 위한 스프링 준비 

💡  스프링의 구조를 보면 ApplicatonContext라는 객체가 존재하고 빈으로 등록된 객체들은  ApplicatonContext 내에 생성되어 관리되는 구조.

    ⚡️  이렇게 만들어진 ApplicatonContext가 웹 애플리케이션에 동작하려면 웹 애플리케이션이 실행될 때  스프링을 로딩해서 해당 웹 애플리케이션 내부에 스프링의 ApplicatonContext를 생성하는 작업이 필요하게 되는데 이를 위해서 web.xml을 이용해서 리스너를 설정

    ⚡️  스프링 프레임워크의 웹과 관련된 작업은 'spring-webmvc'라이브러리를 추가해야만 설정이 가능

 

 

 

 

▶️  build.gradle파일에 spring-mvc 라이브러리를 추가

 

 

 

 

▶️  WEB-INF 폴더 아래 web.xml에  <listener>설정과 <listener>에 필요한 <context-param>을 추가

 

 

 

 

💫  설정이 추가된 후에 톰캣을 실행하면 스프링과 관련된 로그가 기록되면서 실행 


8.  DataSource 구성하기 

톰캣과 스프링이 연동되는 구조를 완성했다면 데이터베이스 관련 설정을 추가

 

  📌  build.gradle에 MySQL과 HikariCP 관련 라이브러리를 추가

    // https://mvnrepository.com/artifact/mysql/mysql-connector-java
    implementation 'mysql:mysql-connector-java:8.0.33'

    // https://mvnrepository.com/artifact/com.zaxxer/HikariCP
    implementation 'com.zaxxer:HikariCP:5.0.1'

 

 

   📌  root-context.xml에 HikariCP 설정하기 

    <context:component-scan base-package="com.example.sping_class.sample" />

    <bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/sample" />
        <property name="username" value="root"/>
        <property name="password" value="비밀번호"/>
        <property name="dataSourceProperties">
            <props>
                <prop key="cachePrepStmts">true</prop>
                <prop key="prepStmtCacheSize">250</prop>
                <prop key="prepStmtCacheSqlLimit">2048</prop>
            </props>
        </property>
    </bean>

    <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
        <constructor-arg ref="hikariConfig" />
    </bean>

 

  • HikariCP를 사용하기 위해서는 HikariConfig 객체와 HikariDataSource를 초기화해야함
  • root-context.xml을 이용해서 HikariConfig 객체와 HikariDataSource 객체를 설정
  • hikariConfig에는 id 속성이 적용되어 있고 HikariDataSource는 <constructor-arg ref="hikariConfig" />로 id 값을 참조해서 사용
  • HikariDataSource는 javax.sql의 DataSource 인터페이스의 구현체이므로 테스트 코드를 통해서 설정에 문제가 없는지 확인

 

 

 

🎃  root-context.xml에 선언된 HikariCP를 주입받기 위해서 DataSource타입의 변수를 선언하고 @Autowired를 이용해서 주입 되도록 구성

 

 

 

 

 

 

 

 

 

 

 

 

 

 

👩🏻‍💻  스프링은 필요한 객체를 스프링에서 주입해 주기 때문에 개별적으로 클래스를 작성해서 빈으로 등록해 두기만 하면 원하는 곳에서 쉽게 다른 객체를 사용할 수 있음. 이런 특징으로 인해 스프링 프레임워크는 웹이나 데이터베이스 같은 특정한 영역이 아닌 전체 애플리케이션의 구조를 설계할 때 사용

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]

 


1. 쿠키와 세션의 차이

출처 :&nbsp;https://cyberchimps.com/blog/session-cookies/

 

출처 :&nbsp;https://twitter.com/sahnlam/status/1759829449284231214

 

쿠키와 세션 응답 헤더

 

  📌  쿠키를 사용했을 때는 아이디와 비밀번호가 노출됨  ▶️  보안이 되지 않음


2.  쿠키  Cookie

html 파일

 

<body>
    <form action="cookie01_process.jsp" method="post">
          <p>아 이 디 : <input type="text" name="id"></p>
          <p>비밀번호 : <input type="text" name="passwd"></p>
          <p><input type="submit" value="전송"></p>
    </form>
</body>

 

jsp 파일

 

    📍 쿠키 생성하기
         1) 전송된 아이디와 비밀번호가 일치하면, 쿠키 이름 userId, userPw에 값을 설정하도록 Cookie 객체를 생성

<body>

    <%
      String userId = request.getParameter("id");
      String userPw = request.getParameter("passwd");

      if (userId.equals("admin") && userPw.equals("1234")) { // 로그인 정보가 맞으면
        Cookie cookieId = new Cookie("userId", userId); // 쿠키 생성. Cookie 클래스를 이용해서 쿠키 객체 생성.
        Cookie cookiePw = new Cookie("userPw", userPw);
        response.addCookie(cookieId); // response 할 때 쿠키도 같이 보낼 것
        response.addCookie(cookiePw);

        out.println("쿠키 생성이 성공했습니다<br>");
        out.println(userId + "님 환영합니다");
      } else {
        out.println("쿠키 생성이 실패했습니다");
      }
    %>

</body>

 

    📍 쿠키 객체에 저장된 모든 쿠키 값 가져와 출력
        1) 쿠키 정보를 얻어오도록 request 내장 객체의 getCookies() 메서드 작성
        2) 얻어온 쿠키 정보의 개수를 출력하도록 Cookie 객체의 length를 작성
        3) 얻어온 쿠키 정보에서 쿠키 이름과 값을 하나씩 출력하도록 Cookie 객체의 getName(), getValue() 메소드 작성

<body>

    <%
      boolean isLogin = false;
      String userId = "";
      Cookie[] cookies = request.getCookies(); // request에서 쿠키를 얻어옴.
      out.println("현재 설정된 쿠키의 개수 => " + cookies.length + "<br>");
      out.println("====================================<br>");
      for (int i = 0; i < cookies.length; i++) {
        out.println("설정된 쿠키의 속성 이름 [ " + i + " ] : " + cookies[i].getName() + "<br>");
        out.println("설정된 쿠키의 속성 값 [ " + i + " ] : " + cookies[i].getValue() + "<br>");
        out.println("--------------------------------------------<br>");

        if (cookies[i].getName().equals("userId") && cookies[i].getValue() != null) {
          // userId라는 쿠키이름이 있고 value가 null이 아니면 로그인 한 것으로 간주
          isLogin = true;
          userId = cookies[i].getValue();
        }
      }

      if (isLogin) {
        out.print(userId + "님이 로그인 중입니다.");
      } else {
        out.print("로그인 상태가 아닙니다.");
      }
    %>

</body>


    📍 쿠키 객체에 저장된 모든 쿠키 삭제하기
        1) 쿠키 정보를 얻어오도록 request 내장 객체의 getCookies() 메소드 작성
        2) 얻어온 모든 쿠키를 삭제하도록 Cookie 객체의 setMaxAge() 메소드에 유효 기간을 0으로 설정
        3) 웹 페이지 cookie02.jsp로 이동하도록 response 내장 객체의 sendRedirect() 메소드를 작성

<body>
    
    <%
        Cookie[] cookies = request.getCookies();

        for (int i = 0; i < cookies.length; i++) {
            cookies[i].setMaxAge(0);
            response.addCookie(cookies[i]);
        }
        response.sendRedirect("04.jsp");
    %>

</body>


3.  세션  Session

html 파일
<body>
    <form action="session01_process.jsp" method="post">
      <p> 아이디 : <input type="text" name="id"></p>
      <p> 비밀번호 : <input type="text" name="passwd"></p>
      <p><input type="submit" value="전송"></p>
    </form>
</body>

 

jsp 파일

 

     📍  세션 설정하기
         1)  전송된 아이디 비번이 일치하면 세션 속성 이름이 userID, userPW에 값을 설정하도록 session 내장 객체의 setAttribute() 메소드 작성
        2) 일치하지 않으면 실패 메시지를 출력

 

      💫  쿠키와는 다르게 response 객체에 세션을 담는 과정은 없음 ▶️ 서버에 저장하기 때문

<body>
    <%
      String userId = request.getParameter("id");
      String userPw = request.getParameter("passwd");

      if (userId.equals("admin") && userPw.equals("1234")) {
        session.setAttribute("userId", userId);
        session.setAttribute("userPw", userPw);
        out.println("세션 설정이 성공했습니다<br>");
        out.println(userId + "님 환영합니다");
      } else {
        out.println("세션 설정이 실패했습니다");
      }
    %>
</body>

 

    📍 세션에 저장된 속성 값 가져와 출력
        - 세션에 저장된 세션 속성 이름 userID, userPW의 속성 값을 가져오도록 session 내장 객체의 getAttribute() 메소드를 작성

<body>
    <%
        String userId = (String) session.getAttribute("userId"); // 세션의 속성값은 Object라서 다운캐스팅을 해야함.
        String userPw = (String) session.getAttribute("userPw");

        out.println("설정된 세션의 속성 값 [1] : " + userId + "<br>");
        out.println("설정된 세션의 속성 값 [2] : " + userPw);
    %>
</body>

    

    📍  세션에 저장된 세션 속성 삭제
       -  세션에 저장된 세션 속성 이름  userID를 삭제하도록 session 내장 객체의 removeAttribute() 메서드를 작성

<body>
    <p><h4>------ 세션을 삭제하기 전 ------</h4></p>
    <%
        String userId = (String) session.getAttribute("userId");
        String userPw = (String) session.getAttribute("userPw");
        out.println("설정된 세션 userId : " + userId + "<br>");
        out.println("설정된 세션 userPw : " + userPw + "<br>");

        session.removeAttribute("userId");
        session.removeAttribute("userPw");
    %>
    
    <p><h4>------ 세션을 삭제한 후 ------</h4></p>
    <%
        userId = (String) session.getAttribute("userId");
        userPw = (String) session.getAttribute("userPw");
        out.println("설정된 세션 userId : " + userId + "<br>");
        out.println("설정된 세션 userPw : " + userPw + "<br>");
    %>
</body>

    📍 세션에 저장된 모든 세션 속성 삭제
        1) 요청에 포함된 클라이언트의 세션이 유효하면 유효한 메시지를 출력하고, 그렇지 않으면 유효하지 않은 메시지를 출력
        2) 세션에 저장된 모든 세션 속성을 삭제하도록 session 내장 객체의 invalidate() 메서드를 작성

 

     👾  request.isRequestSessionIdValue() : request에 포함된 SessionId가 유효한지 검사. 반환형은 boolean

<body>
    <p><h4>------ 세션을 삭제하기 전 ------</h4></p>
    <%
        String userId = (String) session.getAttribute("userId");
        String userPw = (String) session.getAttribute("userPw");
        out.println("설정된 세션 userId : " + userId + "<br>");
        out.println("설정된 세션 userPw : " + userPw + "<br>");

        if (request.isRequestedSessionIdValid() == true) {
          out.print("세션이 유효합니다.");
        } else {
          out.print("세션이 유효하지 않습니다.");
        }

        session.invalidate();
     %>
     
    <p><h4>------ 세션을 삭제한 후 ------</h4></p>
    <%
      if (request.isRequestedSessionIdValid() == true) {
        out.print("세션이 유효합니다.");
      } else {
        out.print("세션이 유효하지 않습니다.");
      }
    %>
</body>

    

    📍 세션 아이디와 웹 사이트에서 유지한 시간 출력


        1) 고유한 세션 내장 객체의 아이디를 가져오도록 session 내장 객체의 getId() 메서드를 작성
        2) 세션에 마지막으로 접근한 시간을 가져오도록 session 내장 객체의 getLastAccessedTime() 메서드를 작성
        3) 세션이 생성된 시간을 가져오도록 session 내장 객체의 getCreationTime() 메서드를 작성
        4) 웹 사이트에 머문 시간을 계산하도록 작성

<body>
    <%
      String sessinId = session.getId();
      // 세션에 마지막으로 접근한 시간
      long lastTime = session.getLastAccessedTime();  // 단위가 1/1,000초
      // 세션이 생성된 시간
      long startTime = session.getCreationTime();  // 단위가 1/1,000초

      long usedTime = (lastTime - startTime) / 1000;

      out.println("세션 아이디 : " + sessinId + "<br>");
      out.println("요청 시작 시간  : " + startTime + "<br>");
      out.println("요청 마지막 시간  : " +  lastTime + "<br>");
      out.println("웹 사이트에서 경과 시간  : " + usedTime + "초<br>");
    %>
</body>

    📍 세션 유효 시간을 가져와 출력


        1) 세션에 설정된 유효 시간을 가져오도록 session 내장 객체의 getMaxInactiveInterval() 메서드를 작성하고, 유효시간을 출력
        2) 세션 유효 시간을 60 * 60 초로 설정하도록 session 내장 객체의 setMaxInactiveInterval() 메서드를 작성
        3) 세션에 설정된 유효 시간을 가져오도록 session 내장 객체의 getMaxInactiveInterval() 메서드를 작성하고, 유효시간을 출력

<body>
    <p><h4>------ 세션 유효시간 변경 전 ------</h4></p>
    <%
        int time = session.getMaxInactiveInterval() / 60;

        out.println("세션 유효 시간 : " + time + "분<br>");
    %>
    <p><h4>------ 세션 유효시간 변경 후 ------</h4></p>
    <%
        session.setMaxInactiveInterval(60 * 60);
        time = session.getMaxInactiveInterval() / 60;

        out.println("세션 유효 시간 : " + time + "분<br>");
    %>
</body>

 

 

 

 

 

 

 

 

 

[ 내용 참고 : IT학원 강의 ]

+ Recent posts