마이크로서비스 시작하기: Dropwizard 자습서

게시 됨: 2022-03-11

우리 모두는 마이크로서비스 아키텍처의 인기 상승을 목격하고 있습니다. 마이크로서비스 아키텍처에서 Dropwizard는 매우 중요한 위치를 차지합니다. RESTful 웹 서비스를 구축하기 위한 프레임워크 또는 보다 정확하게는 RESTful 웹 서비스를 구축하기 위한 도구 및 프레임워크 세트입니다.

개발자는 빠른 프로젝트 부트스트랩을 허용합니다. 이를 통해 프로덕션 환경에서 독립 실행형 서비스로 쉽게 배포할 수 있도록 애플리케이션을 패키징할 수 있습니다. 예를 들어 Spring 프레임워크에서 프로젝트를 부트스트랩해야 하는 상황에 처한 적이 있다면 이것이 얼마나 고통스러운지 알 것입니다.

그림: Dropwizard 자습서의 마이크로서비스 예.

Dropwizard를 사용하면 하나의 Maven 종속성을 추가하기만 하면 됩니다.

이 블로그에서는 간단한 Dropwizard RESTful 서비스를 작성하는 전체 프로세스를 안내합니다. 완료되면 "부품"에 대한 기본 CRUD 작업에 대한 서비스가 제공됩니다. "부분"이 무엇인지는 별로 중요하지 않습니다. 그것은 무엇이든 될 수 있습니다. 그냥 먼저 떠올랐습니다.

쿼리를 위해 JDBI를 사용하여 MySQL 데이터베이스에 데이터를 저장하고 다음 끝점을 사용합니다.

  • GET /parts - DB에서 모든 부분을 검색합니다.
  • GET /part/{id} 를 사용하여 DB에서 특정 부분 가져오기
  • POST /parts - 새 부품 생성
  • PUT /parts/{id} - 기존 부품을 편집합니다.
  • DELETE /parts/{id} -DB에서 부분을 삭제합니다.

OAuth를 사용하여 서비스를 인증하고 마지막으로 몇 가지 단위 테스트를 추가합니다.

기본 Dropwizard 라이브러리

REST 서비스를 별도로 구축하는 데 필요한 모든 라이브러리를 포함하고 각각을 구성하는 대신 Dropwizard가 이를 수행합니다. 다음은 기본적으로 Dropwizard와 함께 제공되는 라이브러리 목록입니다.

  • Jetty: 웹 애플리케이션을 실행하려면 HTTP가 필요합니다. Dropwizard는 웹 애플리케이션 실행을 위한 Jetty 서블릿 컨테이너를 포함합니다. Dropwizard는 응용 프로그램을 응용 프로그램 서버나 웹 서버에 배포하는 대신 Jetty 서버를 독립 실행형 프로세스로 호출하는 기본 메서드를 정의합니다. 현재로서는 Dropwizard는 Jetty에서만 애플리케이션을 실행할 것을 권장합니다. Tomcat과 같은 다른 웹 서비스는 공식적으로 지원되지 않습니다.
  • Jersey: Jersey는 시장에서 가장 우수한 REST API 구현 중 하나입니다. 또한 표준 JAX-RS 사양을 따르며 JAX-RS 사양에 대한 참조 구현입니다. Dropwizard는 Jersey를 RESTful 웹 애플리케이션을 구축하기 위한 기본 프레임워크로 사용합니다.
  • Jackson: Jackson은 JSON 형식 처리를 위한 사실상의 표준입니다. JSON 형식을 위한 최고의 객체 매퍼 API 중 하나입니다.
  • 메트릭: Dropwizard에는 HTTP 끝점을 통해 애플리케이션 메트릭을 노출하기 위한 자체 메트릭 모듈이 있습니다.
  • Guava: 고도로 최적화된 불변 데이터 구조 외에도 Guava는 Java 개발 속도를 높이기 위해 점점 더 많은 클래스를 제공합니다.
  • Logback 및 Slf4j: 이 두 가지는 더 나은 로깅 메커니즘에 사용됩니다.
  • Freemarker 및 Mustache: 애플리케이션을 위한 템플릿 엔진을 선택하는 것은 주요 결정 중 하나입니다. 선택한 템플릿 엔진은 더 나은 스크립트를 작성하기 위해 더 유연해야 합니다. Dropwizard는 사용자 인터페이스를 구축하기 위해 잘 알려져 있고 인기 있는 템플릿 엔진인 Freemarker와 Mustache를 사용합니다.

위의 목록 외에도 REST 서비스를 구축하기 위해 Dropwizard에서 사용하는 Joda Time, Liquibase, Apache HTTP Client 및 Hibernate Validator와 같은 다른 라이브러리가 많이 있습니다.

메이븐 구성

Dropwizard는 공식적으로 Maven을 지원합니다. 다른 빌드 도구를 사용할 수 있더라도 대부분의 가이드 및 문서에서는 Maven을 사용하므로 여기에서도 사용할 것입니다. Maven에 익숙하지 않은 경우 이 Maven 자습서를 확인할 수 있습니다.

이것은 Dropwizard 응용 프로그램을 만드는 첫 번째 단계입니다. Maven의 pom.xml 파일에 다음 항목을 추가하십시오.

 <dependencies> <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-core</artifactId> <version>${dropwizard.version}</version> </dependency> </dependencies>

위 항목을 추가하기 전에 아래와 같이 dropwizard.version 을 추가할 수 있습니다.

 <properties> <dropwizard.version>1.1.0</dropwizard.version> </properties>

그게 다야 Maven 구성 작성을 완료했습니다. 그러면 프로젝트에 필요한 모든 종속성이 다운로드됩니다. 현재 Dropwizard 버전은 1.1.0이므로 이 가이드에서 사용할 것입니다.

이제 첫 번째 실제 Dropwizard 응용 프로그램을 작성할 수 있습니다.

구성 클래스 정의

Dropwizard는 구성을 YAML 파일에 저장합니다. 애플리케이션 루트 폴더에 configuration.yml 파일이 있어야 합니다. 그런 다음 이 파일은 애플리케이션 구성 클래스의 인스턴스로 역직렬화되고 유효성이 검사됩니다. 애플리케이션의 구성 파일은 Dropwizard의 구성 클래스( io.dropwizard.Configuration )의 하위 클래스입니다.

간단한 구성 클래스를 만들어 보겠습니다.

 import javax.validation.Valid; import javax.validation.constraints.NotNull; import com.fasterxml.jackson.annotation.JsonProperty; import io.dropwizard.Configuration; import io.dropwizard.db.DataSourceFactory; public class DropwizardBlogConfiguration extends Configuration { private static final String DATABASE = "database"; @Valid @NotNull private DataSourceFactory dataSourceFactory = new DataSourceFactory(); @JsonProperty(DATABASE) public DataSourceFactory getDataSourceFactory() { return dataSourceFactory; } @JsonProperty(DATABASE) public void setDataSourceFactory(final DataSourceFactory dataSourceFactory) { this.dataSourceFactory = dataSourceFactory; } }

YAML 구성 파일은 다음과 같습니다.

 database: driverClass: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost/dropwizard_blog user: dropwizard_blog password: dropwizard_blog maxWaitForConnection: 1s validationQuery: "SELECT 1" validationQueryTimeout: 3s minSize: 8 maxSize: 32 checkConnectionWhileIdle: false evictionInterval: 10s minIdleTime: 1 minute checkConnectionOnBorrow: true

위의 클래스는 YAML 파일에서 역직렬화되고 YAML 파일의 값을 이 개체에 넣습니다.

애플리케이션 클래스 정의

이제 메인 애플리케이션 클래스를 생성해야 합니다. 이 클래스는 모든 번들을 함께 가져와 응용 프로그램을 실행하고 사용할 수 있도록 합니다.

다음은 Dropwizard의 응용 프로그램 클래스의 예입니다.

 import io.dropwizard.Application; import io.dropwizard.auth.AuthDynamicFeature; import io.dropwizard.auth.oauth.OAuthCredentialAuthFilter; import io.dropwizard.setup.Environment; import javax.sql.DataSource; import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature; import org.skife.jdbi.v2.DBI; import com.toptal.blog.auth.DropwizardBlogAuthenticator; import com.toptal.blog.auth.DropwizardBlogAuthorizer; import com.toptal.blog.auth.User; import com.toptal.blog.config.DropwizardBlogConfiguration; import com.toptal.blog.health.DropwizardBlogApplicationHealthCheck; import com.toptal.blog.resource.PartsResource; import com.toptal.blog.service.PartsService; public class DropwizardBlogApplication extends Application<DropwizardBlogConfiguration> { private static final String SQL = "sql"; private static final String DROPWIZARD_BLOG_SERVICE = "Dropwizard blog service"; private static final String BEARER = "Bearer"; public static void main(String[] args) throws Exception { new DropwizardBlogApplication().run(args); } @Override public void run(DropwizardBlogConfiguration configuration, Environment environment) { // Datasource configuration final DataSource dataSource = configuration.getDataSourceFactory().build(environment.metrics(), SQL); DBI dbi = new DBI(dataSource); // Register Health Check DropwizardBlogApplicationHealthCheck healthCheck = new DropwizardBlogApplicationHealthCheck(dbi.onDemand(PartsService.class)); environment.healthChecks().register(DROPWIZARD_BLOG_SERVICE, healthCheck); // Register OAuth authentication environment.jersey() .register(new AuthDynamicFeature(new OAuthCredentialAuthFilter.Builder<User>() .setAuthenticator(new DropwizardBlogAuthenticator()) .setAuthorizer(new DropwizardBlogAuthorizer()).setPrefix(BEARER).buildAuthFilter())); environment.jersey().register(RolesAllowedDynamicFeature.class); // Register resources environment.jersey().register(new PartsResource(dbi.onDemand(PartsService.class))); } }

위에서 실제로 수행된 작업은 Dropwizard 실행 메서드를 재정의하는 것입니다. 이 방법에서는 DB 연결을 인스턴스화하고, 사용자 지정 상태 확인을 등록하고(나중에 설명하겠습니다), 서비스에 대한 OAuth 인증을 초기화하고, 마지막으로 Dropwizard 리소스를 등록합니다.

이들 모두는 나중에 설명될 것입니다.

표현 클래스 정의

이제 REST API와 리소스의 표현에 대해 생각하기 시작해야 합니다. JSON 형식과 원하는 JSON 형식으로 변환하는 해당 표현 클래스를 설계해야 합니다.

이 간단한 표현 클래스 예제에 대한 샘플 JSON 형식을 살펴보겠습니다.

 { "code": 200, "data": { "id": 1, "name": "Part 1", "code": "PART_1_CODE" } }

위의 JSON 형식의 경우 아래와 같이 표현 클래스를 생성합니다.

 import org.hibernate.validator.constraints.Length; import com.fasterxml.jackson.annotation.JsonProperty; public class Representation<T> { private long code; @Length(max = 3) private T data; public Representation() { // Jackson deserialization } public Representation(long code, T data) { this.code = code; this.data = data; } @JsonProperty public long getCode() { return code; } @JsonProperty public T getData() { return data; } }

이것은 상당히 간단한 POJO입니다.

리소스 클래스 정의

리소스는 REST 서비스의 모든 것입니다. 서버의 리소스에 액세스하기 위한 끝점 URI일 뿐입니다. 이 예에서는 요청 URI 매핑에 대한 주석이 거의 없는 리소스 클래스가 있습니다. Dropwizard는 JAX-RS 구현을 사용하므로 @Path 주석을 사용하여 URI 경로를 정의합니다.

다음은 Dropwizard 예제의 리소스 클래스입니다.

 import java.util.List; import javax.annotation.security.RolesAllowed; import javax.validation.Valid; import javax.validation.constraints.NotNull; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.eclipse.jetty.http.HttpStatus; import com.codahale.metrics.annotation.Timed; import com.toptal.blog.model.Part; import com.toptal.blog.representation.Representation; import com.toptal.blog.service.PartsService; @Path("/parts") @Produces(MediaType.APPLICATION_JSON) @RolesAllowed("ADMIN") public class PartsResource { private final PartsService partsService;; public PartsResource(PartsService partsService) { this.partsService = partsService; } @GET @Timed public Representation<List<Part>> getParts() { return new Representation<List<Part>>(HttpStatus.OK_200, partsService.getParts()); } @GET @Timed @Path("{id}") public Representation<Part> getPart(@PathParam("id") final int id) { return new Representation<Part>(HttpStatus.OK_200, partsService.getPart(id)); } @POST @Timed public Representation<Part> createPart(@NotNull @Valid final Part part) { return new Representation<Part>(HttpStatus.OK_200, partsService.createPart(part)); } @PUT @Timed @Path("{id}") public Representation<Part> editPart(@NotNull @Valid final Part part, @PathParam("id") final int id) { part.setId(id); return new Representation<Part>(HttpStatus.OK_200, partsService.editPart(part)); } @DELETE @Timed @Path("{id}") public Representation<String> deletePart(@PathParam("id") final int id) { return new Representation<String>(HttpStatus.OK_200, partsService.deletePart(id)); } }

모든 엔드포인트가 실제로 이 클래스에 정의되어 있는 것을 볼 수 있습니다.

리소스 등록

이제 기본 응용 프로그램 클래스로 돌아가겠습니다. 해당 클래스의 끝에서 서비스 실행으로 초기화할 리소스를 등록한 것을 볼 수 있습니다. 애플리케이션에 있는 모든 리소스를 사용하여 그렇게 해야 합니다. 다음은 이를 담당하는 코드 스니펫입니다.

 // Register resources environment.jersey().register(new PartsResource(dbi.onDemand(PartsService.class)));

서비스 계층

적절한 예외 처리 및 데이터 스토리지 엔진과 독립적인 기능을 위해 "중간 계층" 서비스 클래스를 도입할 것입니다. 이것은 리소스 계층에서 호출할 클래스이며 기본이 무엇인지 신경 쓰지 않습니다. 이것이 리소스 레이어와 DAO 레이어 사이에 이 레이어가 있는 이유입니다. 다음은 서비스 클래스입니다.

 import java.util.List; import java.util.Objects; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response.Status; import org.skife.jdbi.v2.exceptions.UnableToExecuteStatementException; import org.skife.jdbi.v2.exceptions.UnableToObtainConnectionException; import org.skife.jdbi.v2.sqlobject.CreateSqlObject; import com.toptal.blog.dao.PartsDao; import com.toptal.blog.model.Part; public abstract class PartsService { private static final String PART_NOT_FOUND = "Part id %s not found."; private static final String DATABASE_REACH_ERROR = "Could not reach the MySQL database. The database may be down or there may be network connectivity issues. Details: "; private static final String DATABASE_CONNECTION_ERROR = "Could not create a connection to the MySQL database. The database configurations are likely incorrect. Details: "; private static final String DATABASE_UNEXPECTED_ERROR = "Unexpected error occurred while attempting to reach the database. Details: "; private static final String SUCCESS = "Success..."; private static final String UNEXPECTED_ERROR = "An unexpected error occurred while deleting part."; @CreateSqlObject abstract PartsDao partsDao(); public List<Part> getParts() { return partsDao().getParts(); } public Part getPart(int id) { Part part = partsDao().getPart(id); if (Objects.isNull(part)) { throw new WebApplicationException(String.format(PART_NOT_FOUND, id), Status.NOT_FOUND); } return part; } public Part createPart(Part part) { partsDao().createPart(part); return partsDao().getPart(partsDao().lastInsertId()); } public Part editPart(Part part) { if (Objects.isNull(partsDao().getPart(part.getId()))) { throw new WebApplicationException(String.format(PART_NOT_FOUND, part.getId()), Status.NOT_FOUND); } partsDao().editPart(part); return partsDao().getPart(part.getId()); } public String deletePart(final int id) { int result = partsDao().deletePart(id); switch (result) { case 1: return SUCCESS; case 0: throw new WebApplicationException(String.format(PART_NOT_FOUND, id), Status.NOT_FOUND); default: throw new WebApplicationException(UNEXPECTED_ERROR, Status.INTERNAL_SERVER_ERROR); } } public String performHealthCheck() { try { partsDao().getParts(); } catch (UnableToObtainConnectionException ex) { return checkUnableToObtainConnectionException(ex); } catch (UnableToExecuteStatementException ex) { return checkUnableToExecuteStatementException(ex); } catch (Exception ex) { return DATABASE_UNEXPECTED_ERROR + ex.getCause().getLocalizedMessage(); } return null; } private String checkUnableToObtainConnectionException(UnableToObtainConnectionException ex) { if (ex.getCause() instanceof java.sql.SQLNonTransientConnectionException) { return DATABASE_REACH_ERROR + ex.getCause().getLocalizedMessage(); } else if (ex.getCause() instanceof java.sql.SQLException) { return DATABASE_CONNECTION_ERROR + ex.getCause().getLocalizedMessage(); } else { return DATABASE_UNEXPECTED_ERROR + ex.getCause().getLocalizedMessage(); } } private String checkUnableToExecuteStatementException(UnableToExecuteStatementException ex) { if (ex.getCause() instanceof java.sql.SQLSyntaxErrorException) { return DATABASE_CONNECTION_ERROR + ex.getCause().getLocalizedMessage(); } else { return DATABASE_UNEXPECTED_ERROR + ex.getCause().getLocalizedMessage(); } } }

마지막 부분은 실제로 상태 확인 구현입니다. 이에 대해서는 나중에 설명하겠습니다.

DAO 계층, JDBI 및 매퍼

Dropwizard는 JDBI와 최대 절전 모드를 지원합니다. 별도의 Maven 모듈이므로 먼저 종속성과 MySQL 커넥터로 추가해 보겠습니다.

 <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-jdbi</artifactId> <version>${dropwizard.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.connector.version}</version> </dependency>

간단한 CRUD 서비스의 경우 구현이 더 간단하고 훨씬 빠르기 때문에 개인적으로 JDBI를 선호합니다. 우리의 예에서 사용할 하나의 테이블만 있는 간단한 MySQL 스키마를 만들었습니다. 소스 내에서 스키마에 대한 초기화 스크립트를 찾을 수 있습니다. JDBI는 읽기용 @SqlQuery 및 데이터 쓰기용 @SqlUpdate와 같은 주석을 사용하여 간단한 쿼리 쓰기를 제공합니다. 다음은 DAO 인터페이스입니다.

 import java.util.List; import org.skife.jdbi.v2.sqlobject.Bind; import org.skife.jdbi.v2.sqlobject.BindBean; import org.skife.jdbi.v2.sqlobject.SqlQuery; import org.skife.jdbi.v2.sqlobject.SqlUpdate; import org.skife.jdbi.v2.sqlobject.customizers.RegisterMapper; import com.toptal.blog.mapper.PartsMapper; import com.toptal.blog.model.Part; @RegisterMapper(PartsMapper.class) public interface PartsDao { @SqlQuery("select * from parts;") public List<Part> getParts(); @SqlQuery("select * from parts where id = :id") public Part getPart(@Bind("id") final int id); @SqlUpdate("insert into parts(name, code) values(:name, :code)") void createPart(@BindBean final Part part); @SqlUpdate("update parts set name = coalesce(:name, name), code = coalesce(:code, code) where id = :id") void editPart(@BindBean final Part part); @SqlUpdate("delete from parts where id = :id") int deletePart(@Bind("id") final int id); @SqlQuery("select last_insert_id();") public int lastInsertId(); }

보시다시피 상당히 간단합니다. 그러나 매퍼 클래스를 등록하여 SQL 결과 세트를 모델에 매핑해야 합니다. 다음은 매퍼 클래스입니다.

 import java.sql.ResultSet; import java.sql.SQLException; import org.skife.jdbi.v2.StatementContext; import org.skife.jdbi.v2.tweak.ResultSetMapper; import com.toptal.blog.model.Part; public class PartsMapper implements ResultSetMapper<Part> { private static final String; private static final String NAME = "name"; private static final String CODE = "code"; public Part map(int i, ResultSet resultSet, StatementContext statementContext) throws SQLException { return new Part(resultSet.getInt(ID), resultSet.getString(NAME), resultSet.getString(CODE)); } }

그리고 우리의 모델:

 import org.hibernate.validator.constraints.NotEmpty; public class Part { private int id; @NotEmpty private String name; @NotEmpty private String code; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public Part() { super(); } public Part(int id, String name, String code) { super(); this.id = id; this.name = name; this.code = code; } }

Dropwizard 상태 확인

Dropwizard는 상태 확인에 대한 기본 지원을 제공합니다. 우리의 경우 서비스가 정상이라고 말하기 전에 데이터베이스가 작동하고 실행 중인지 확인하고 싶을 것입니다. 우리가 하는 일은 실제로 DB에서 부품을 가져오고 잠재적인 결과(성공 또는 예외)를 처리하는 것과 같은 몇 가지 간단한 DB 작업을 수행하는 것입니다.

다음은 Dropwizard의 상태 확인 구현입니다.

 import com.codahale.metrics.health.HealthCheck; import com.toptal.blog.service.PartsService; public class DropwizardBlogApplicationHealthCheck extends HealthCheck { private static final String HEALTHY = "The Dropwizard blog Service is healthy for read and write"; private static final String UNHEALTHY = "The Dropwizard blog Service is not healthy. "; private static final String MESSAGE_PLACEHOLDER = "{}"; private final PartsService partsService; public DropwizardBlogApplicationHealthCheck(PartsService partsService) { this.partsService = partsService; } @Override public Result check() throws Exception { String mySqlHealthStatus = partsService.performHealthCheck(); if (mySqlHealthStatus == null) { return Result.healthy(HEALTHY); } else { return Result.unhealthy(UNHEALTHY + MESSAGE_PLACEHOLDER, mySqlHealthStatus); } } }

인증 추가

Dropwizard는 기본 인증 및 OAuth를 지원합니다. 여기. OAuth로 서비스를 보호하는 방법을 알려 드리겠습니다. 그러나 복잡성으로 인해 기본 DB 구조를 생략하고 랩핑하는 방법만 보여주었습니다. 여기에서 시작하여 전체 규모로 구현하는 것이 문제가 되어서는 안 됩니다. Dropwizard에는 구현해야 하는 두 가지 중요한 인터페이스가 있습니다.

첫 번째는 인증자입니다. 우리 클래스는 주어진 액세스 토큰이 유효한지 확인해야 하는 authenticate 메소드를 구현해야 합니다. 그래서 나는 이것을 응용 프로그램의 첫 번째 관문이라고 부를 것입니다. 성공하면 주체를 반환해야 합니다. 이 보안 주체는 역할이 있는 실제 사용자입니다. 이 역할은 구현해야 하는 다른 Dropwizard 인터페이스에 중요합니다. 이것은 Authorizer이며 사용자에게 특정 리소스에 액세스할 수 있는 충분한 권한이 있는지 확인하는 역할을 합니다. 따라서 돌아가서 리소스 클래스를 확인하면 끝점에 액세스하기 위해 관리자 역할이 필요하다는 것을 알 수 있습니다. 이러한 주석은 메서드별로도 가능합니다. Dropwizard 인증 지원은 별도의 Maven 모듈이므로 종속성에 추가해야 합니다.

 <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-auth</artifactId> <version>${dropwizard.version}</version> </dependency>

다음은 실제로 스마트한 작업을 수행하지 않는 예제의 클래스이지만 전체 규모의 OAuth 인증을 위한 골격입니다.

 import java.util.Optional; import io.dropwizard.auth.AuthenticationException; import io.dropwizard.auth.Authenticator; public class DropwizardBlogAuthenticator implements Authenticator<String, User> { @Override public Optional<User> authenticate(String token) throws AuthenticationException { if ("test_token".equals(token)) { return Optional.of(new User()); } return Optional.empty(); } }
 import java.util.Objects; import io.dropwizard.auth.Authorizer; public class DropwizardBlogAuthorizer implements Authorizer<User> { @Override public boolean authorize(User principal, String role) { // Allow any logged in user. if (Objects.nonNull(principal)) { return true; } return false; } }
 import java.security.Principal; public class User implements Principal { private int id; private String username; private String password; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String getName() { return username; } }

Dropwizard의 단위 테스트

애플리케이션에 몇 가지 단위 테스트를 추가해 보겠습니다. 저는 코드의 Dropwizard 특정 부분(우리의 경우 Representation 및 Resource)을 계속 테스트할 것입니다. Maven 파일에 다음 종속성을 추가해야 합니다.

 <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-testing</artifactId> <version>${dropwizard.version}</version> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>${mockito.version}</version> <scope>test</scope> </dependency>

표현을 테스트하려면 테스트할 샘플 JSON 파일도 필요합니다. 이제 src/test/resources 아래에 fixtures/part.json 을 생성해 보겠습니다.

 { "id": 1, "name": "testPartName", "code": "testPartCode" }

다음은 JUnit 테스트 클래스입니다.

 import static io.dropwizard.testing.FixtureHelpers.fixture; import static org.assertj.core.api.Assertions.assertThat; import org.junit.Test; import com.fasterxml.jackson.databind.ObjectMapper; import com.toptal.blog.model.Part; import io.dropwizard.jackson.Jackson; public class RepresentationTest { private static final ObjectMapper MAPPER = Jackson.newObjectMapper(); private static final String PART_JSON = "fixtures/part.json"; private static final String TEST_PART_NAME = "testPartName"; private static final String TEST_PART_CODE = "testPartCode"; @Test public void serializesToJSON() throws Exception { final Part part = new Part(1, TEST_PART_NAME, TEST_PART_CODE); final String expected = MAPPER.writeValueAsString(MAPPER.readValue(fixture(PART_JSON), Part.class)); assertThat(MAPPER.writeValueAsString(part)).isEqualTo(expected); } @Test public void deserializesFromJSON() throws Exception { final Part part = new Part(1, TEST_PART_NAME, TEST_PART_CODE); assertThat(MAPPER.readValue(fixture(PART_JSON), Part.class).getId()).isEqualTo(part.getId()); assertThat(MAPPER.readValue(fixture(PART_JSON), Part.class).getName()) .isEqualTo(part.getName()); assertThat(MAPPER.readValue(fixture(PART_JSON), Part.class).getCode()) .isEqualTo(part.getCode()); } }

리소스 테스트와 관련하여 Dropwizard 테스트의 요점은 실제로 HTTP 클라이언트로 행동하여 리소스에 대해 HTTP 요청을 보내는 것입니다. 따라서 일반적인 경우에 일반적으로 수행하는 방법을 테스트하지 않습니다. 다음은 PartsResource 클래스의 예입니다.

 public class PartsResourceTest { private static final String SUCCESS = "Success..."; private static final String TEST_PART_NAME = "testPartName"; private static final String TEST_PART_CODE = "testPartCode"; private static final String PARTS_ENDPOINT = "/parts"; private static final PartsService partsService = mock(PartsService.class); @ClassRule public static final ResourceTestRule resources = ResourceTestRule.builder().addResource(new PartsResource(partsService)).build(); private final Part part = new Part(1, TEST_PART_NAME, TEST_PART_CODE); @Before public void setup() { when(partsService.getPart(eq(1))).thenReturn(part); List<Part> parts = new ArrayList<>(); parts.add(part); when(partsService.getParts()).thenReturn(parts); when(partsService.createPart(any(Part.class))).thenReturn(part); when(partsService.editPart(any(Part.class))).thenReturn(part); when(partsService.deletePart(eq(1))).thenReturn(SUCCESS); } @After public void tearDown() { reset(partsService); } @Test public void testGetPart() { Part partResponse = resources.target(PARTS_ENDPOINT + "/1").request() .get(TestPartRepresentation.class).getData(); assertThat(partResponse.getId()).isEqualTo(part.getId()); assertThat(partResponse.getName()).isEqualTo(part.getName()); assertThat(partResponse.getCode()).isEqualTo(part.getCode()); verify(partsService).getPart(1); } @Test public void testGetParts() { List<Part> parts = resources.target(PARTS_ENDPOINT).request().get(TestPartsRepresentation.class).getData(); assertThat(parts.size()).isEqualTo(1); assertThat(parts.get(0).getId()).isEqualTo(part.getId()); assertThat(parts.get(0).getName()).isEqualTo(part.getName()); assertThat(parts.get(0).getCode()).isEqualTo(part.getCode()); verify(partsService).getParts(); } @Test public void testCreatePart() { Part newPart = resources.target(PARTS_ENDPOINT).request() .post(Entity.entity(part, MediaType.APPLICATION_JSON_TYPE), TestPartRepresentation.class) .getData(); assertNotNull(newPart); assertThat(newPart.getId()).isEqualTo(part.getId()); assertThat(newPart.getName()).isEqualTo(part.getName()); assertThat(newPart.getCode()).isEqualTo(part.getCode()); verify(partsService).createPart(any(Part.class)); } @Test public void testEditPart() { Part editedPart = resources.target(PARTS_ENDPOINT + "/1").request() .put(Entity.entity(part, MediaType.APPLICATION_JSON_TYPE), TestPartRepresentation.class) .getData(); assertNotNull(editedPart); assertThat(editedPart.getId()).isEqualTo(part.getId()); assertThat(editedPart.getName()).isEqualTo(part.getName()); assertThat(editedPart.getCode()).isEqualTo(part.getCode()); verify(partsService).editPart(any(Part.class)); } @Test public void testDeletePart() { assertThat(resources.target(PARTS_ENDPOINT + "/1").request() .delete(TestDeleteRepresentation.class).getData()).isEqualTo(SUCCESS); verify(partsService).deletePart(1); } private static class TestPartRepresentation extends Representation<Part> { } private static class TestPartsRepresentation extends Representation<List<Part>> { } private static class TestDeleteRepresentation extends Representation<String> { } }

Dropwizard 애플리케이션 구축

가장 좋은 방법은 애플리케이션을 실행하는 데 필요한 모든 .class 파일을 포함하는 단일 FAT JAR 파일을 빌드하는 것입니다. 종속성 라이브러리를 변경하지 않고도 동일한 JAR 파일을 테스트에서 프로덕션까지 다른 환경에 배포할 수 있습니다. 예제 애플리케이션을 팻 JAR로 빌드하기 시작하려면 maven-shade라는 Maven 플러그인을 구성해야 합니다. pom.xml 파일의 플러그인 섹션에 다음 항목을 추가해야 합니다.

다음은 JAR 파일을 빌드하기 위한 샘플 Maven 구성입니다.

 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.endava</groupId> <artifactId>dropwizard-blog</artifactId> <version>0.0.1-SNAPSHOT</version> <name>Dropwizard Blog example</name> <properties> <dropwizard.version>1.1.0</dropwizard.version> <mockito.version>2.7.12</mockito.version> <mysql.connector.version>6.0.6</mysql.connector.version> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-core</artifactId> <version>${dropwizard.version}</version> </dependency> <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-jdbi</artifactId> <version>${dropwizard.version}</version> </dependency> <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-auth</artifactId> <version>${dropwizard.version}</version> </dependency> <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-testing</artifactId> <version>${dropwizard.version}</version> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>${mockito.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.connector.version}</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>2.3</version> <configuration> <createDependencyReducedPom>true</createDependencyReducedPom> <filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" /> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.endava.blog.DropwizardBlogApplication</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin> </plugins> </build> </project>

애플리케이션 실행

이제 서비스를 실행할 수 있어야 합니다. JAR 파일을 성공적으로 빌드했다면 명령 프롬프트를 열고 다음 명령을 실행하여 JAR 파일을 실행하기만 하면 됩니다.

 java -jar target/dropwizard-blog-1.0.0.jar server configuration.yml

모든 것이 정상이면 다음과 같이 표시됩니다.

 INFO [2017-04-23 22:51:14,471] org.eclipse.jetty.util.log: Logging initialized @962ms to org.eclipse.jetty.util.log.Slf4jLog INFO [2017-04-23 22:51:14,537] io.dropwizard.server.DefaultServerFactory: Registering jersey handler with root path prefix: / INFO [2017-04-23 22:51:14,538] io.dropwizard.server.DefaultServerFactory: Registering admin handler with root path prefix: / INFO [2017-04-23 22:51:14,681] io.dropwizard.server.DefaultServerFactory: Registering jersey handler with root path prefix: / INFO [2017-04-23 22:51:14,681] io.dropwizard.server.DefaultServerFactory: Registering admin handler with root path prefix: / INFO [2017-04-23 22:51:14,682] io.dropwizard.server.ServerFactory: Starting DropwizardBlogApplication INFO [2017-04-23 22:51:14,752] org.eclipse.jetty.setuid.SetUIDListener: Opened application@7d57dbb5{HTTP/1.1,[http/1.1]}{0.0.0.0:8080} INFO [2017-04-23 22:51:14,752] org.eclipse.jetty.setuid.SetUIDListener: Opened admin@630b6190{HTTP/1.1,[http/1.1]}{0.0.0.0:8081} INFO [2017-04-23 22:51:14,753] org.eclipse.jetty.server.Server: jetty-9.4.2.v20170220 INFO [2017-04-23 22:51:15,153] io.dropwizard.jersey.DropwizardResourceConfig: The following paths were found for the configured resources: GET /parts (com.toptal.blog.resource.PartsResource) POST /parts (com.toptal.blog.resource.PartsResource) DELETE /parts/{id} (com.toptal.blog.resource.PartsResource) GET /parts/{id} (com.toptal.blog.resource.PartsResource) PUT /parts/{id} (com.toptal.blog.resource.PartsResource) INFO [2017-04-23 22:51:15,154] org.eclipse.jetty.server.handler.ContextHandler: Started idjMutableServletContextHandler@58fa5769{/,null,AVAILABLE} INFO [2017-04-23 22:51:15,158] io.dropwizard.setup.AdminEnvironment: tasks = POST /tasks/log-level (io.dropwizard.servlets.tasks.LogConfigurationTask) POST /tasks/gc (io.dropwizard.servlets.tasks.GarbageCollectionTask) INFO [2017-04-23 22:51:15,162] org.eclipse.jetty.server.handler.ContextHandler: Started idjMutableServletContextHandler@3fdcde7a{/,null,AVAILABLE} INFO [2017-04-23 22:51:15,176] org.eclipse.jetty.server.AbstractConnector: Started application@7d57dbb5{HTTP/1.1,[http/1.1]}{0.0.0.0:8080} INFO [2017-04-23 22:51:15,177] org.eclipse.jetty.server.AbstractConnector: Started admin@630b6190{HTTP/1.1,[http/1.1]}{0.0.0.0:8081} INFO [2017-04-23 22:51:15,177] org.eclipse.jetty.server.Server: Started @1670ms

Now you have your own Dropwizard application listening on ports 8080 for application requests and 8081 for administration requests.

Note that server configuration.yml is used for starting the HTTP server and passing the YAML configuration file location to the server.

훌륭한! Finally, we have implemented a microservice using Dropwizard framework. Now let's go for a break and have a cup of tea. You have done really good job.

리소스 액세스

You can use any HTTP client like POSTMAN or any else. You should be able to access your server by hitting http://localhost:8080/parts . You should be receiving a message that the credentials are required to access the service. To authenticate, add Authorization header with bearer test_token value. If done successfully, you should see something like:

 { "code": 200, "data": [] }

meaning that your DB is empty. Create your first part by switching HTTP method from GET to POST, and supply this payload:

 { "name":"My first part", "code":"code_of_my_first_part" }

All other endpoints work in the same manner, so keep playing and enjoy.

How to Change Context Path

By default, Dropwizard application will start and running in the / . For example, if you are not mentioning anything about the context path of the application, by default, the application can be accessed from the URL http://localhost:8080/ . If you want to configure your own context path for your application, then please add the following entries to your YAML file.

 server: applicationContextPath: /application

Wrapping up our Dropwizard Tutorial

Now when you have your Dropwizard REST service up and running, let's summarize some key advantages or disadvantages of using Dropwizard as a REST framework. It's absolutely obvious from this post that Dropwizard offers extremely fast bootstrap of your project. And that's probably the biggest advantage of using Dropwizard.

Also, it will include all the cutting-edge libraries/tools that you will ever need in developing your service. So you definitely do not need to worry about that. It also gives you very nice configuration management. Of course, Dropwizard has some disadvantages as well. By using Dropwizard, you're kind of restricted to using what Dropwizard offers or supports. You lose some of the freedom you may be used to when developing. But still, I wouldn't even call it a disadvantage, as this is exactly what makes Dropwizard what it is—easy to set up, easy to develop, but yet a very robust and high-performance REST framework.

In my opinion, adding complexity to the framework by supporting more and more third party libraries would also introduce unnecessary complexity in development.