본문 바로가기

Multi Tenancy

[Spring Boot] 스키마 기반 멀티테넌시 구현 (1/2)

728x90

H2 데이터베이스를 활용하여, 스키마 기반으로 멀티테넌시를 구현하는 방법을 알아보도록 하겠습니다.

(스프링 부트 버전은 2.6.10 입니다.)

 

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

우선 pom.xml을 위와 같이 설정해줍니다. (Flyway라는 마이그레이션 도구도 사용할 예정입니다.)

 

 

@Entity
public class User implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String username;

    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    private String password;

    // getters, setters and overriden methods from UserDetails
}
@Entity
public class Note {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String text;
    
    // getters and setters
}

엔티티 클래스를 생성합니다.

두 클래스 모두 롬복 플러그인을 이용하여 @Getter @Setter 메서드를 생성해 주시고,

User 클래스의 경우 UserDetails의 메서드를 오버라이딩 해주세요.

getPassword() 와 같은 메서드가 클래스의 비밀번호를 반환하도록 설정해 주시고,

isAccountNonExpired()와 같은 메서드는 모두 true로 설정해주세요.

 

 

@Repository
public interface UserRepository extends CrudRepository<User, Long> {
    Optional<User> findByUsername(String username);
}
@Repository
public interface NoteRepository extends CrudRepository<Note, Long> {
}

엔티티 CRUD를 위한 레파지토리를 생성합니다.

 

 

@Service
public class UserService implements UserDetailsService {

  private UserRepository repository;
  private PasswordEncoder encoder;
  private TenantService tenantService;

  public UserService(UserRepository repository, TenantService tenantService) {
    this.repository = repository;
    this.tenantService = tenantService;
    this.encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
  }

  @Transactional
  public User createUser(User user) {
    String encodedPassword = encoder.encode(user.getPassword());
    user.setPassword(encodedPassword);
    User saved = repository.save(user);
    tenantService.initDatabase(user.getUsername());
    return saved;
  }

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    return repository.findByUsername(username)
        .orElseThrow(() -> new UsernameNotFoundException("User with the specified username is not found"));
  }
}
@Service
public class NoteService {

  private NoteRepository repository;

  public NoteService(NoteRepository repository) {
    this.repository = repository;
  }

  public Note createNote(Note note) {
    return repository.save(note);
  }

  public Note findNote(Long id) {
    return repository.findById(id).orElseThrow();
  }

  public Iterable<Note> findAllNotes() {
    return repository.findAll();
  }
}

엔티티 별 서비스도 구현해줍니다.

 

 

@RestController
@RequestMapping("/users")
public class UserController {

    private UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping
    public ResponseEntity<User> register(@RequestBody User user) {
        User created = userService.createUser(user);
        return ResponseEntity.status(HttpStatus.CREATED).body(created);
    }
}
@RestController
@RequestMapping("/notes")
public class NoteController {

    private NoteService noteService;

    public NoteController(NoteService noteService) {
        this.noteService = noteService;
    }

    @PostMapping
    public ResponseEntity<Note> createNote(@RequestBody Note note) {
        Note created = noteService.createNote(note);
        return ResponseEntity.status(HttpStatus.CREATED).body(created);
    }

    @GetMapping("/{id}")
    public ResponseEntity<Note> getNote(@PathVariable Long id) {
        Note note = noteService.findNote(id);
        return ResponseEntity.ok(note);
    }

    @GetMapping
    public ResponseEntity<Iterable<Note>> getAllNotes() {
        Iterable<Note> notes = noteService.findAllNotes();
        return ResponseEntity.ok(notes);
    }
}

API 호출을 위한 컨트롤러입니다.

 

 

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private UserDetailsService userService;

    public SecurityConfig(UserDetailsService userService) {
        this.userService = userService;
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/h2-console/**").permitAll()
                .antMatchers(HttpMethod.POST, "/users").permitAll()
                .anyRequest().authenticated()
                .and()
                .httpBasic()
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .csrf().disable()
                .headers().frameOptions().disable();
    }
}

WebSecurityConfigurerAdapter가 Deprecated된걸로 알고있는데 이 부분은 다른 포스팅을 통해 다루도록 하겠습니다.

우선 프로젝트 기본 세팅을 마쳤구요, 다음 포스팅에 멀티테넌시 기능 구현 및 테스트 업로드하도록 하겠습니다.

 

 

ref : https://sultanov.dev/blog/schema-based-multi-tenancy-with-spring-data/

728x90

'Multi Tenancy' 카테고리의 다른 글

[Spring Boot] 스키마 기반 멀티테넌시 구현 (2/2)  (5) 2022.08.09