스터디/[토비의 스프링 3.1 Vol 1] (2025.03)

[토비의 스프링 3.1 Vol 1] 1장. 오브젝트와 의존관계

ttoance 2025. 3. 9. 01:40
반응형

들어가며 : 스프링이란 무엇인가 ?

  • 자바 엔터프라이즈 애플리케이션 개발에 사용되는 애플리케이션 프레임워크
    • 기본 틀 - 스프링 컨테이너
      • 스프링은 스프링 컨테이너 (또는 애플리케이션 컨텍스트) 라고 불리는 스프링 런타임 엔진을 제공
      • 스프링 컨테이너는 설정 정보를 참고로 해서 애플리케이션을 구성하는 오브젝트를 생성하고 관리 
      • 스프링 컨테이너는 독립적으로 동작할 수도 있지만 보통 웹 모듈에서 동작하는 서비스나 서블릿으로 등록해서 사용 
    • 공통 프로그래밍 모델 - IoC/DI, 서비스 추상화, AOP
      • 1) IoC/DI : 오브젝트의 생명주기와 의존관계에 대한 프로그래밍 모델으로 스프링 프레임워크에 동작하는 코드는 IoC/DI 방식을 따라서 작성돼야 스프링이 제공하는 가치를 제대로 누릴 수 있다.
      • 2) 서비스 추상화 : 스프링을 사용하면 환경이나 서버, 특정 기술에 종속되지 않고 이식성이 뛰어나며 유연한 애플리케이션을 만들 수 있는데 이 때 환경에 종속되지 않도록 유연한 추상 계층을 둘 수 있다. 
      • 3) AOP : 애플리케이션 코드에 산재해서 나타나는 부가적인 기능을 독립적으로 모듈화하는 프로그래밍 모델이다. 

 

1장. 오브젝트와 의존관계 

1.1 초난감 DAO 

  • 사용자 정보를 JDBC API를 통해 DB에 저장하고 조회할 수 있는 간단한 DAO를 하나 만들어보자.

 

User.java

package springbook.user.domain;

public class User {
	String id;
	String name;
	String password;
	
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getPassword() {
		return password;
	}
	public void setPassword(String password) {
		this.password = password;
	}
}

 

UserDao.java

package springbook.user.dao;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import springbook.user.domain.User;

public class UserDao {
	public void add(User user) throws ClassNotFoundException, SQLException {
		Class.forName("com.mysql.jdbc.Driver");
		Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook?characterEncoding=UTF-8", "spring",
				"book");

		PreparedStatement ps = c.prepareStatement(
			"insert into users(id, name, password) values(?,?,?)");
		ps.setString(1, user.getId());
		ps.setString(2, user.getName());
		ps.setString(3, user.getPassword());

		ps.executeUpdate();

		ps.close();
		c.close();
	}


	public User get(String id) throws ClassNotFoundException, SQLException {
		Class.forName("com.mysql.jdbc.Driver");
		Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook?characterEncoding=UTF-8", "spring",
				"book");
		PreparedStatement ps = c
				.prepareStatement("select * from users where id = ?");
		ps.setString(1, id);

		ResultSet rs = ps.executeQuery();
		rs.next();
		User user = new User();
		user.setId(rs.getString("id"));
		user.setName(rs.getString("name"));
		user.setPassword(rs.getString("password"));

		rs.close();
		ps.close();
		c.close();

		return user;
	}

}

 

1.1.3 main()을 이용한 DAO 테스트 코드 

public static void main(String[] args) throws ClassNotFoundException, SQLException {
		UserDao dao = new UserDao();

		User user = new User();
		user.setId("whiteship");
		user.setName("백기선");
		user.setPassword("married");

		dao.add(user);
			
		System.out.println(user.getId() + " 등록 성공");
		
		User user2 = dao.get(user.getId());
		System.out.println(user2.getName());
		System.out.println(user2.getPassword());
			
		System.out.println(user2.getId() + " 조회 성공");
	}

 

 

1.2 DAO의 분리 

1.2.1 관심사의 분리

  • 사용자의 비즈니스 프로세스와 그에 따른 요구사항은 끈임없이 발전하고 바뀐다. 
  • 그래서 개발자가 객체를 설계할 때 가장 중요한 사항은 바로 미래의 변화를 어떻게 대비할 것인가이다. 
  • 가장 좋은 대책은 변화의 폭을 최소한으로 줄여주는 것이다. 
  • ex) 단지 DB 접속용 암호를 변경하려고 DAO 클래스 수백 개를 모두 수정해야 한다면 ?

분리와 확장을 고려한 설계가 있어야 한다. 
프로그래밍의 기초 개념 중에 관심사의 분리 Seperation of Concerns 라는 게 있다. 이를 객체지향에 적용하면, 관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모이게 하고, 관심이 다른 것은 가능한 한 따로 떨어져서 서로 영향을 주지 않도록 분리해야 한다. 

 

1.2.2 커넥션 만들기의 추출 

UserDao의 관심사항 

  • DB 연결을 위한 커넥션을 어떻게 가져올까 
  • 사용자 등록을 위해 DB에 보낼 SQL 문장을 담을 Statement을 만들고 실행하는 것 
  • 작업이 끝나면 사용한 리소스인 Statement와 Connection 오브젝트를 닫아줘서 소중한 공유 리소스를 시스템에 돌려주는 것 

 

DB 연결을 위한 커넥션을 어떻게 가져올까  => 중복 코드의 메소드 추출 extract method 기법 

 extract method 기법 
  • DB 종류와 접속 방법이 바뀌거나 로그인 정보가 변경돼어도 getConnection 한 메소드만 수정하면 된다. 

 

 

1.2.3 DB 커넥션 만들기의 독립 

템플릿 메소드 패턴 template method pattern 
팩토리 메소드 패턴 factory method pattern

  • 핵심 기능인 데이터를 등록하고 가져오는 기능은 UserDao가, 
  • DB 연결 방법은 어떻게 할 것인가라는 관심을 담고 있는 NUserDao, DUserDao가 클래스 레벨로 구분이 되고 있다. 
// UserDao.java
public abstract class UserDao {
	public void add(User user) throws ClassNotFoundException, SQLException {
    }
}


// NUserDao.java
package springbook.user.dao;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class NUserDao extends UserDao {
	protected Connection getConnection() throws ClassNotFoundException,
			SQLException {
		Class.forName("com.mysql.jdbc.Driver");
		Connection c = DriverManager.getConnection(
				"jdbc:mysql://localhost/springbook?characterEncoding=UTF-8",
				"spring", "book");
		return c;
	}
}

// DUserDao.java
package springbook.user.dao;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DUserDao extends UserDao {
	protected Connection getConnection() throws ClassNotFoundException,
			SQLException {
		Class.forName("com.mysql.jdbc.Driver");
		Connection c = DriverManager.getConnection(
				"jdbc:mysql://localhost/springbook?characterEncoding=UTF-8",
				"spring", "book");
		return c;
	}
}

 

1.3 DAO의 확장 

1.3.1 클래스의 분리 

package springbook.user.dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import springbook.user.domain.User;

public abstract class UserDao {
	private SimpleConnectionMaker simpleConnectionMaker;
	
	public UserDao() {
		this.simpleConnectionMaker = new SimpleConnectionMaker();
	}

	public void add(User user) throws ClassNotFoundException, SQLException {
		Connection c = this.simpleConnectionMaker.getConnection();

	.....

}

 

SimpleConnectionMaker.java

package springbook.user.dao;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class SimpleConnectionMaker {
	public Connection getConnection() throws ClassNotFoundException,
			SQLException {
		Class.forName("com.mysql.jdbc.Driver");
		Connection c = DriverManager.getConnection(
				"jdbc:mysql://localhost/springbook?characterEncoding=UTF-8", "spring", "book");
		return c;
	}
}

 

  • 두 가지 문제를 해결해야 한다. 
    • 첫째는 SimpleConnectionMaker에서 DB 커넥션을 가져오는 함수가 변경이 된다면 add(), get() 메소드 커넥션을 가져오는 코드를 일일이 변경해야 한다. 
    • 두번째는 DB 커넥션을 제공하는 클래스가 어떤 것인지를 UserDao가 구체적으로 알고 있어야 한다.
문제의 근본적인 원인은 UserDao가 바뀔 수 있는 정보, 즉 DB 커넥션을 가져오는 클래스에 대해 너무 많이 알고 있기 때문이다. 
어떤 클래스가 쓰일지, 그 클래스에서 가져오는 메소드는 이름이 뭔지까지 일일이 알고 있어야 한다. 

 

 

 

 

1.3.2 인터페이스의 도입

  • 해결책은 두 개의 클래스가 서로 긴밀하게 연결되어 있지 않도록 중간에 추상적인 느슨한 연결고리를 만들어주는 것이다. 

인터페이스는 어떤 일을 하겠다는 기능만 정의해놓은 것이다. 
UserDao가 인터페이스를 사용하게 한다면 인터페이스의 메소드를 통해 알 수 있는 기능에만 관심을 가지면 되지, 그 기능을 어떻게 구현했는지에는 관심을 둘 필요가 없다. 

 

 

1.3.3 관계설정 책임의 분리 

  • 인터페이스를 써가면서 완벽하게 분리했는데도, UserDao가 구체적인 클래스를 알아야 한다는 문제가 발생한다. 
    • UserDao에는 어떤 ConnectionMaker 구현 클래스를 사용할지를 결정하는 new DConnectionMaker()라는 코드가 존재한다. 
    • 이 관심사를 담은 코드를 UserDao에서 분리하지 않으면 UserDao는 결코 독립적으로 확장 가능한 클래스가 될 수 없다. 
  • UserDao의 클라이언트라고 하면 UserDao를 사용하는 오브젝트를 가리킬 수 있다.
    • 바로 이 클래스가 UserDao와 ConnectionMaker 구현 클래스의 관심 관계를 결정해주는 기능을 분리해서 두기에 적절한 곳이다. 
    • 오브젝트 사이에 런타임 사용관계 또는 링크, 또는 의존관계라고 불리는 관계를 맺어주면 된다. 
package springbook.user.dao;

import java.sql.SQLException;

import springbook.user.domain.User;

public class UserDaoTest {
	public static void main(String[] args) throws ClassNotFoundException, SQLException {
		ConnectionMaker connectionMaker = new DConnectionMaker();
		UserDao dao = new UserDao(connectionMaker);
		...
	}
}
  • 이렇게되면 이제부터 UserDao는 자신의 관심사이자 책임인 제이터 엑세스 작업을 위해 SQL 생성하고, 이를 실행하는 데만 집중할 수 있다. 

 

 

1.3.4 원칙과 패턴 

개방 폐쇄 원칙 OCP, Open-Closed Principle 

  • 클래스나 모듈은 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.
    • UserDao는 DB 연결 방법이라는 기능을 확장하는 데는 열려 있다. => UserDao에 전혀 영향을 주지 않고도 얼마든지 기능을 확장할 수 있다.
    • 동시에 UserDao 자신의 핵심 기능을 구현한 코드는 그런 변화에 영향을 받지 않고 유지할 수 있으므로 변경에는 닫혀 있다고 말할 수 있다.  

 

높은 응집도와 낮은 결합도 High Coherence And Low Coupling 

  • 높은 응집도
    • 하나의 모듈, 클래스가 하나의 책임 또는 관심사에만 집중되어 있다는 뜻이다. 
    • 변경이 일어날 때 모듈의 많은 부분이 함께 바뀐다면 응집도가 높다고 말할 수 있다. 
      • 만약 모듈의 일부분에만 변경이 일어나도 된다면, 모듈 전체에서 어떤 부분이 바뀌어야 하는지 파악해야 하고, 
      • 또 그 변경으로 인해 바뀌지 않는 부분에는 다른 영향을 미치지는 않은지 확인해야 하는 이중의 부담이 생긴다. 
  • 낮은 결합도 
    • 높은 응집도보다 더 민감한 원칙이다.
    • 책임과 관심사가 다른 오브젝트 또는 모듈과는 낮은 결함도를 유지하는 것이 바람직하다. 

 

전략 패턴 

  • 개선한 UserDaoTest - UserDao - ConnectionMaker 구조를 디자인 패턴의 시각으로 보면 전략 패턴이다. 
  • 전략 패턴은 자신의 기능 맥락에서, 필요에 따라 변경이 필요한 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴 

 

 

1.4 제어의 역전 (IoC)

1.4.1 오브젝트 팩토리 

  • 객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 것인데, 이런 일을 하는 오브젝트를 팩토리 Factory 라고 한다. 
// UserFactory.java
package springbook.user.dao;

public class UserDaoFactory {
	public UserDao userDao() {
		UserDao dao = new UserDao(connectionMaker());
		return dao;
	}

...
}



// UserDaoTest.java
package springbook.user.dao;

import java.sql.SQLException;

import springbook.user.domain.User;

public class UserDaoTest {
	public static void main(String[] args) throws ClassNotFoundException, SQLException {
		UserDao dao = new UserDaoFactory().userDao();

		...
	}
}

 

 

1.4.2 오브젝트 팩토리의 활용

  • AccountDao, MessageDao를 만든다고 했을때, ConnectionMaker 구현 클래스의 오브젝트를 생성하는 코드가 메소드마다 반복되게 된다. 

 

 

  • extract method 기법을 써서 중복된 함수를 뽑아낸다.

 

 

1.4.3 제어권의 이전을 통한 제어관계 역전 

 

제어의 역전 Inversion Of Control 

  • 제어의 역전이라는 건, 간단히 프로그램 제어 흐름 구조가 뒤바뀌는 것이라고 설명할 수 있다. 
  • 제어의 역전에서는 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하지 않는다. 당연히 생성하지도 않는다.
    • 또 자신이 어떻게 만들어지고 어디서 사용되는지를 알 수 없다. 모든 제어 권한을 자신이 아닌 다른 대상에게 위임하기 때문이다. 
  • 프레임워크도 제어의 역전 개념이 적용된 대표적인 기술이다. 
    • 라이브러리를 사용하는 애플리케이션 코드는 애플리케이션 흐름을 직접 제어한다. 
    • 반면에 프레임워크는 거꾸로 애플리케이션 코드가 프레임워크에 의해 사용된다. 

 

1.5 스프링의 IoC

1.5.1 오브젝트 팩토리를 이용한 스프링 IoC

애플리케이션 컨텍스트와 설정정보 

  • 스프링에서는 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트를 빈 bean 이라고 부른다.
  • 스프링 빈은 스프링 컨테이너가 생성과 관계설정, 사용 등을 제어해주는 제어의 역전이 적용된 오브젝트를 가리키는 말이다. 
  • 빈의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트를 빈 팩토리 bean factory 라고 부른다. 
    • 보통 bean factory는 좀 더 확장한 애플리케이션 컨텍스트 application context 라고도 한다. 
    • application context는 IoC 방식을 따라 만들어진 일종의 빈 팩토리라고 생각하면 된다. 

 

DaoFactory를 사용하는 애플리케이션 컨텍스트

  • 먼서 스프링이 빈 팩토리를 위한 오브젝트 설정을 담당하는 클래스라고 인식할 수 있도록 @Configuration이라는 애노테이션을 추가한다. 그리고 오브젝트를 만들어주는 메소드에는 @Bean이라는 애노테이션을 붙여준다. 
package springbook.user.dao;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DaoFactory {
	@Bean
	public UserDao userDao() {
		UserDao dao = new UserDao(connectionMaker());
		return dao;
	}

	@Bean
	public ConnectionMaker connectionMaker() {
		ConnectionMaker connectionMaker = new DConnectionMaker();
		return connectionMaker;
	}
}
  • 그리고 @Configuration이 붙은 자바코드를 설정정보로 사용하려면 AnnotationConfigApplicationContext를 이용하면 된다. 
package springbook.user.dao;

import java.sql.SQLException;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import springbook.user.domain.User;

public class UserDaoTest {
	public static void main(String[] args) throws ClassNotFoundException, SQLException {
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
		UserDao dao = context.getBean("userDao", UserDao.class);

		...
	}
}

 

 

1.5.2 애플리케이션 컨텍스트의 동작방식 

  • 오브젝트 팩토리에 대응되는 것이 스프링의 애플리케이션 컨텍스트다.
    • 스프링에서는 이 애플리케이션 컨텍스트를 IoC 컨테이너라 하기도 하고, 간단히 스프링 컨테이너 라고도 한다. 또는 빈 팩토리라고 부를 수도 있다. 
    • DaoFactory가 UserDao를 비롯한 DAO 오브젝트를 생성하고 DB 생성 오브젝트와 관계를 맺어주는 제한적인 역할을 하는 데 반해, 애플리케이션 컨텍스트는 애플리케이션 IoC를 적용해서 관리할 모든 오브젝트에 대한 생성과 관계설정을 담당한다. 

  • 장점은 다음과 같다.
    • 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다. 
      • 애플리케이션 컨텍스트를 사용하면 오브젝트 팩토리가 아무리 많아져도 이를 알아야 하거나 직접 사용할 필요가 없다.
      • 애플리케이션 컨텍스트를 이용하면 일관된 방식으로 원하는 오브젝트를 가져올 수 있다.
      • 또, DaoFactory 처럼 자바 코드를 작성하는 대신 XML 처럼 단순한 방법 사용해 애플리케이션 컨텍스트가 사용할 IoC 설정정보를 만들 수도 있다. 
    • 애플리케이션 컨텍스트는 종합 IoC 서비스를 제공해준다. 
      • 단지 오브젝트 생성과 다른 오브젝트의 관계설정만이 전부가 아니라, 오브젝트가 만들어지는 방식, 시점과 전략을 다르게 가져갈 수도 있고, 부가적으로 자동생성, 후처리 등 다양한 기능을 제공한다.
    • 애플리케이션 컨텍스트는 빈을 검색한 다양한 방법을 제공한다.
      • getBean() 메소드는 빈의 이름을 이용해 빈을 찾아줄 수도 있고, 타입만으로 빈을 검색하거나 특별한 애노테이션 설정이 되어 있는 빈을 찾을 수 있다. 

 

1.5.3 스프링 IoC의 용어 정리 

  • 빈 bean 
    • 스프링이 IoC 방식으로 관리하는 오브젝트
    • 주의할 점은 스프링을 사용하는 애플리케이션에서 만들어지는 모든 오브젝트가 다 빈은 아니고, 그 중에서 스프링이 직접 그 생성과 제어를 담당하는 오브젝트만을 빈 bean 이라고 한다. 
  • 빈 팩토리 bean factory
    • 스프링 IoC를 담당하는 핵심 컨테이너
    • BeanFactory라고 붙여쓰면 빈 팩토리가 구현하는 가장 기본적인 인터페이스의 이름이 된다. 
  • 애플리케이션 컨텍스트 application context
    • 빈 팩토리를 확장한 IoC 컨테이너
    • 빈을 등록하고 관리하는 기본적인 기능은 빈 팩토리와 동일하다.
    • 여기에 스프링이 제공하는 각종 부가 서비스를 추가로 제공한다. 
  • 설정정보/설정 메타정보 configuration metadata
    • 애플리케이션 컨텍스트 또는 빈 팩토리가 IoC를 적용하기 위해 사용하는 메타정보 
    • IoC 컨테이너에 의해 관리되는 애플리케이션 오브젝트를 생성하고 구성할 때 사용한다. 
  • 컨테이너 또는 IoC 컨테이너
    • IoC 방식으로 빈을 관리한다는 의미에서 애플리케이션 컨텍스트나 빈 팩토리를 컨테이너 또는 IoC 컨테이너라고 한다. 
    • 컨테이너라는 말 자체가 IoC의 개념을 담고 있기 때문에 이름이 긴 애플리케이션 컨텍스트 대신에 스프링 컨테이너라고 부르기를 선호한다. 
  • 스프링 프레임워크
    • IoC 컨테이너, 애플리케이션 컨텍스트를 포함해서 스프링이 제공하는 모든 기능을 통틀어 말할 때 주로 사용한다. 

 

1.6 싱글톤 레지스트리와 오브젝트 스코프

DaoFactory를 직접 사용하는 것과 @Configuration 애노테이션을 추가해서 스프링의 애플리케이션 컨텍스트 사용하는 것 비교 

  • DaoFactory의 userDao()를 여러번 호출했을때 동일한 오브젝트일까 
    • 코드를 보면 매번 new 연산자에 의해 돌아올 것이라고 예상할 수 있다. 
    • userDao를 매번 호출하면 계속해서 새로운 오브젝트가 만들어진다. 
  • 스프링의 애플리케이션 컨텍스트를 사용하면  오브젝트가 동일한 것을 알 수 있다. 

 

1.6.1 싱글톤 레지스트리로서의 애플리케이션 컨텍스트 

  • 스프링은 기본적으로 별다른 설정을 하지 않으면 내부에서 생성하는 빈 오브젝트를 모두 싱글톤으로 만든다. 
  • 왜 스프링은 싱글톤으로 빈을 만드는 것일까 ?
    • 이는 스프링이 주로 적용되는 대상이 자바 엔터프라이즈 기술을 사용하는 서버환경이기 때문이다. 
    • 매번 클라이언트에서 요청이 올 때마다 각 로직을 담당하는 오브젝트를 새로 만든다면, 부하가 걸릴수가 있다. 

 

  • 싱글톤 구현 방식
  1. 클래스 밖에서는 객체를 생성하지 못하도록 생성자를 private으로 만든다.
  2. 생성된 싱글톤 객체를 저장할 수 있는 자신과 같은 타입의 스태틱 필드를 정의한다.
  3. 스태틱 팩토리 메서드인 getInstance()를 만들고 이 메서드가 최초로 호출되는 시점에서 한 번만 객체를 생성하게 한다. 생성된 객체는 스태틱 필드에 저장된다. 또는 스태틱 필드의 초기값으로 객체를 미리 만들어 둘 수도 있다.
  4. 한 번 객체(싱글톤)가 만들어지고 난 후에는 getInstance() 메서드를 통해 이미 만들어진 스태틱 필드에 저장된 객체를 넘겨준다.
package springbook.user.dao;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import springbook.user.domain.User;

public class UserDao {
	private static UserDao INSTANCE;
	
	public UserDao(ConnectionMaker simpleConnectionMaker) {
		this.connectionMaker = simpleConnectionMaker;
	}
    
    public static synchronized UserDao getInstance) {
    	if (INSTANCE == null) INSTANCE = new UserDao(??);
        return INSTANCE;
    }

	

}

 

 

 

 

토비 스프링 소스코드 ▶ https://github.com/AcornPublishing/toby-spring3-1/tree/main

 

GitHub - AcornPublishing/toby-spring3-1: 토비의 스프링 3.1

토비의 스프링 3.1. Contribute to AcornPublishing/toby-spring3-1 development by creating an account on GitHub.

github.com

 

토비 스프링 ▶ https://product.kyobobook.co.kr/detail/S000000935358

 

토비의 스프링 3.1 Vol 1: 스프링의 이해와 원리 | 이일민 - 교보문고

토비의 스프링 3.1 Vol 1: 스프링의 이해와 원리 | 대한민국 전자정부 표준 프레임워크 스프링을 설명하는 No. 1 베스트셀러! 단순한 예제를 스프링 3.0과 스프링 3.1의 기술을 적용하며 발전시켜 나

product.kyobobook.co.kr

 

 

반응형